diff --git a/.changeset/silver-apples-build.md b/.changeset/silver-apples-build.md new file mode 100644 index 000000000..cb837562d --- /dev/null +++ b/.changeset/silver-apples-build.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': minor +--- + +feat: allow inlining standalone clients with `client.inline = true` diff --git a/.nvmrc b/.nvmrc index f3f52b42d..d5a159609 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.9.0 +20.10.0 diff --git a/packages/openapi-ts/package.json b/packages/openapi-ts/package.json index 8ebf84e25..c71a6ddba 100644 --- a/packages/openapi-ts/package.json +++ b/packages/openapi-ts/package.json @@ -81,6 +81,7 @@ "@angular/platform-browser": "17.3.9", "@angular/platform-browser-dynamic": "17.3.9", "@angular/router": "17.3.9", + "@hey-api/client-fetch": "workspace:*", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "15.2.3", "@types/cross-spawn": "6.0.6", diff --git a/packages/openapi-ts/src/compiler/module.ts b/packages/openapi-ts/src/compiler/module.ts index 18f7bc703..caf6f8ff3 100644 --- a/packages/openapi-ts/src/compiler/module.ts +++ b/packages/openapi-ts/src/compiler/module.ts @@ -9,35 +9,43 @@ import { /** * Create export all declaration. Example: `export * from './y'`. - * @param module - module to export from. + * @param module - module containing exports * @returns ts.ExportDeclaration */ -export const createExportAllDeclaration = (module: string) => - ts.factory.createExportDeclaration( +export const createExportAllDeclaration = ({ + module, +}: { + module: string; +}): ts.ExportDeclaration => { + const statement = ts.factory.createExportDeclaration( undefined, false, undefined, ots.string(module), ); + return statement; +}; type ImportExportItem = ImportExportItemObject | string; /** * Create a named export declaration. Example: `export { X } from './y'`. - * @param items - the items to export. - * @param module - module to export it from. - * @returns ExportDeclaration + * @param exports - named imports to export + * @param module - module containing exports + * @returns ts.ExportDeclaration */ -export const createNamedExportDeclarations = ( - items: Array | ImportExportItem, - module: string, -): ts.ExportDeclaration => { - items = Array.isArray(items) ? items : [items]; - const exportedTypes = Array.isArray(items) ? items : [items]; +export const createNamedExportDeclarations = ({ + exports, + module, +}: { + exports: Array | ImportExportItem; + module: string; +}): ts.ExportDeclaration => { + const exportedTypes = Array.isArray(exports) ? exports : [exports]; const hasNonTypeExport = exportedTypes.some( (item) => typeof item !== 'object' || !item.asType, ); - const elements = items.map((name) => { + const elements = exportedTypes.map((name) => { const item = typeof name === 'string' ? { name } : name; return ots.export({ alias: item.alias, @@ -101,15 +109,18 @@ export const createExportConstVariable = ({ /** * Create a named import declaration. Example: `import { X } from './y'`. - * @param items - the items to export. - * @param module - module to export it from. - * @returns ImportDeclaration + * @param imports - named exports to import + * @param module - module containing imports + * @returns ts.ImportDeclaration */ -export const createNamedImportDeclarations = ( - items: Array | ImportExportItem, - module: string, -): ts.ImportDeclaration => { - const importedTypes = Array.isArray(items) ? items : [items]; +export const createNamedImportDeclarations = ({ + imports, + module, +}: { + imports: Array | ImportExportItem; + module: string; +}): ts.ImportDeclaration => { + const importedTypes = Array.isArray(imports) ? imports : [imports]; const hasNonTypeImport = importedTypes.some( (item) => typeof item !== 'object' || !item.asType, ); diff --git a/packages/openapi-ts/src/generate/__tests__/class.spec.ts b/packages/openapi-ts/src/generate/__tests__/class.spec.ts index 6e2a93c53..a33e0ce06 100644 --- a/packages/openapi-ts/src/generate/__tests__/class.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/class.spec.ts @@ -11,7 +11,9 @@ vi.mock('node:fs'); describe('generateClientClass', () => { it('writes to filesystem', async () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/generate/__tests__/core.spec.ts b/packages/openapi-ts/src/generate/__tests__/core.spec.ts index b860755cb..3f43e32af 100644 --- a/packages/openapi-ts/src/generate/__tests__/core.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/core.spec.ts @@ -25,7 +25,9 @@ describe('generateCore', () => { }; setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, @@ -82,7 +84,9 @@ describe('generateCore', () => { }; const config = setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, @@ -122,7 +126,9 @@ describe('generateCore', () => { const config = setConfig({ base: 'foo', - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/generate/__tests__/index.spec.ts b/packages/openapi-ts/src/generate/__tests__/index.spec.ts index 9d372e56e..8644bb507 100644 --- a/packages/openapi-ts/src/generate/__tests__/index.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/index.spec.ts @@ -12,7 +12,9 @@ vi.mock('node:fs'); describe('generateIndexFile', () => { it('writes to filesystem', async () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/generate/__tests__/output.spec.ts b/packages/openapi-ts/src/generate/__tests__/output.spec.ts index 388e471cf..7685f5ec3 100644 --- a/packages/openapi-ts/src/generate/__tests__/output.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/output.spec.ts @@ -11,7 +11,9 @@ vi.mock('node:fs'); describe('generateOutput', () => { it('writes to filesystem', async () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts b/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts index 0d87fd901..2504b1cbe 100644 --- a/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/schemas.spec.ts @@ -13,7 +13,9 @@ vi.mock('node:fs'); describe('generateSchemas', () => { it('writes to filesystem', async () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/generate/__tests__/services.spec.ts b/packages/openapi-ts/src/generate/__tests__/services.spec.ts index f95b8c7e4..66a38852b 100644 --- a/packages/openapi-ts/src/generate/__tests__/services.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/services.spec.ts @@ -13,7 +13,9 @@ vi.mock('node:fs'); describe('generateServices', () => { it('writes to filesystem', async () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, @@ -129,7 +131,9 @@ describe('methodNameBuilder', () => { it('use default name', async () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, @@ -169,7 +173,9 @@ describe('methodNameBuilder', () => { const methodNameBuilder = vi.fn().mockReturnValue('customName'); setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, @@ -212,7 +218,9 @@ describe('methodNameBuilder', () => { const methodNameBuilder = vi.fn().mockReturnValue('customName'); setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/generate/__tests__/types.spec.ts b/packages/openapi-ts/src/generate/__tests__/types.spec.ts index b38fb6f93..046a4b285 100644 --- a/packages/openapi-ts/src/generate/__tests__/types.spec.ts +++ b/packages/openapi-ts/src/generate/__tests__/types.spec.ts @@ -12,7 +12,9 @@ vi.mock('node:fs'); describe('generateTypes', () => { it('writes to filesystem', async () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/generate/client.ts b/packages/openapi-ts/src/generate/client.ts new file mode 100644 index 000000000..fe2f0283f --- /dev/null +++ b/packages/openapi-ts/src/generate/client.ts @@ -0,0 +1,56 @@ +import { copyFileSync, readFileSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import { getConfig, isStandaloneClient } from '../utils/config'; +import { ensureDirSync } from './utils'; + +const require = createRequire(import.meta.url); + +/** + * (optional) Creates a `client.ts` file containing the same exports as a + * standalone client package. Creates a `core` directory containing the modules + * from standalone client. These files are generated only when `client.inline` + * is set to true. + */ +export const generateClient = async ( + outputPath: string, + moduleName: string, +) => { + const config = getConfig(); + + if (!isStandaloneClient(config) || !config.client.inline) { + return; + } + + // create directory for client modules + const dirPath = path.resolve(outputPath, 'core'); + ensureDirSync(dirPath); + + const clientModulePath = path.normalize(require.resolve(moduleName)); + const clientModulePathComponents = clientModulePath.split(path.sep); + const clientSrcPath = [ + ...clientModulePathComponents.slice( + 0, + clientModulePathComponents.indexOf('dist'), + ), + 'src', + ].join(path.sep); + + // copy client modules + const files = ['index.ts', 'types.ts', 'utils.ts']; + files.forEach((file) => { + copyFileSync( + path.resolve(clientSrcPath, file), + path.resolve(dirPath, file), + ); + }); + + // copy index file with cherry-picked exports + const nodeIndexFile = readFileSync( + path.resolve(clientSrcPath, 'node', 'index.ts'), + 'utf-8', + ); + const indexFile = nodeIndexFile.replaceAll('../', './core/'); + writeFileSync(path.resolve(outputPath, 'client.ts'), indexFile, 'utf-8'); +}; diff --git a/packages/openapi-ts/src/generate/core.ts b/packages/openapi-ts/src/generate/core.ts index e512c9b87..541479d47 100644 --- a/packages/openapi-ts/src/generate/core.ts +++ b/packages/openapi-ts/src/generate/core.ts @@ -68,7 +68,7 @@ export const generateCore = async ( ...context, }), ); - if (config.client !== 'angular') { + if (config.client.name !== 'angular') { await writeFileSync( path.resolve(outputPath, 'CancelablePromise.ts'), templates.core.cancelablePromise({ diff --git a/packages/openapi-ts/src/generate/indexFile.ts b/packages/openapi-ts/src/generate/indexFile.ts index 8132bedfe..8ce97893a 100644 --- a/packages/openapi-ts/src/generate/indexFile.ts +++ b/packages/openapi-ts/src/generate/indexFile.ts @@ -9,37 +9,50 @@ export const generateIndexFile = async ({ const config = getConfig(); if (config.name) { - files.index.add(compiler.export.named([config.name], `./${config.name}`)); + files.index.add( + compiler.export.named({ + exports: config.name, + module: `./${config.name}`, + }), + ); } if (config.exportCore) { - files.index.add(compiler.export.named('ApiError', './core/ApiError')); + files.index.add( + compiler.export.named({ + exports: 'ApiError', + module: './core/ApiError', + }), + ); if (config.services.response === 'response') { files.index.add( - compiler.export.named( - { asType: true, name: 'ApiResult' }, - './core/ApiResult', - ), + compiler.export.named({ + exports: { asType: true, name: 'ApiResult' }, + module: './core/ApiResult', + }), ); } if (config.name) { files.index.add( - compiler.export.named('BaseHttpRequest', './core/BaseHttpRequest'), + compiler.export.named({ + exports: 'BaseHttpRequest', + module: './core/BaseHttpRequest', + }), ); } - if (config.client !== 'angular') { + if (config.client.name !== 'angular') { files.index.add( - compiler.export.named( - ['CancelablePromise', 'CancelError'], - './core/CancelablePromise', - ), + compiler.export.named({ + exports: ['CancelablePromise', 'CancelError'], + module: './core/CancelablePromise', + }), ); } files.index.add( - compiler.export.named( - ['OpenAPI', { asType: true, name: 'OpenAPIConfig' }], - './core/OpenAPI', - ), + compiler.export.named({ + exports: ['OpenAPI', { asType: true, name: 'OpenAPIConfig' }], + module: './core/OpenAPI', + }), ); } @@ -48,6 +61,10 @@ export const generateIndexFile = async ({ return; } - files.index.add(compiler.export.all(`./${file.getName(false)}`)); + files.index.add( + compiler.export.all({ + module: `./${file.getName(false)}`, + }), + ); }); }; diff --git a/packages/openapi-ts/src/generate/output.ts b/packages/openapi-ts/src/generate/output.ts index 890b3627c..8617d24ee 100644 --- a/packages/openapi-ts/src/generate/output.ts +++ b/packages/openapi-ts/src/generate/output.ts @@ -1,4 +1,3 @@ -import { existsSync, mkdirSync } from 'node:fs'; import path from 'node:path'; import { TypeScriptFile } from '../compiler'; @@ -7,12 +6,14 @@ import type { Client } from '../types/client'; import { getConfig } from '../utils/config'; import type { Templates } from '../utils/handlebars'; import { generateClientClass } from './class'; +import { generateClient } from './client'; import { generateCore } from './core'; import { generateIndexFile } from './indexFile'; import { generateSchemas } from './schemas'; import { generateServices } from './services'; import { generateResponseTransformers } from './transformers'; import { generateTypes } from './types'; +import { ensureDirSync } from './utils'; /** * Write our OpenAPI client, using the given templates at the given output @@ -41,9 +42,7 @@ export const generateOutput = async ( const outputPath = path.resolve(config.output.path); - if (!existsSync(outputPath)) { - mkdirSync(outputPath, { recursive: true }); - } + ensureDirSync(outputPath); const files: Record = { index: new TypeScriptFile({ @@ -76,6 +75,8 @@ export const generateOutput = async ( }); }); + await generateClient(outputPath, config.client.name); + // types.gen.ts await generateTypes({ client, files }); diff --git a/packages/openapi-ts/src/generate/services.ts b/packages/openapi-ts/src/generate/services.ts index 077e55136..c55661c17 100644 --- a/packages/openapi-ts/src/generate/services.ts +++ b/packages/openapi-ts/src/generate/services.ts @@ -153,7 +153,7 @@ const toOperationReturnType = (client: Client, operation: Operation) => { returnType = compiler.typedef.basic('ApiResult', [returnType]); } - if (config.client === 'angular') { + if (config.client.name === 'angular') { returnType = compiler.typedef.basic('Observable', [returnType]); } else { returnType = compiler.typedef.basic('CancelablePromise', [returnType]); @@ -476,7 +476,7 @@ const toOperationStatements = ( ]; } - if (config.client === 'angular') { + if (config.client.name === 'angular') { return [ compiler.return.functionCall({ args: ['OpenAPI', 'this.http', options], @@ -587,7 +587,7 @@ const processService = ( const node = compiler.class.method({ accessLevel: 'public', comment: toOperationComment(operation), - isStatic: config.name === undefined && config.client !== 'angular', + isStatic: config.name === undefined && config.client.name !== 'angular', name: toOperationName(operation, false), parameters: toOperationParamType(client, operation), returnType: isStandalone @@ -623,7 +623,7 @@ const processService = ( }), ...members, ]; - } else if (config.client === 'angular') { + } else if (config.client.name === 'angular') { members = [ compiler.class.constructor({ multiLine: false, @@ -642,7 +642,7 @@ const processService = ( const statement = compiler.class.create({ decorator: - config.client === 'angular' + config.client.name === 'angular' ? { args: [{ providedIn: 'root' }], name: 'Injectable' } : undefined, members, @@ -685,8 +685,8 @@ export const generateServices = async ({ // Import required packages and core files. if (isStandaloneClient(config)) { - files.services?.addImport( - [ + files.services?.addImport({ + imports: [ 'client', { asType: true, @@ -694,42 +694,57 @@ export const generateServices = async ({ }, ...clientImports.filter(unique), ], - config.client, - ); + module: config.client.inline ? './client' : config.client.name, + }); } else { - if (config.client === 'angular') { - files.services?.addImport('Injectable', '@angular/core'); + if (config.client.name === 'angular') { + files.services?.addImport({ + imports: 'Injectable', + module: '@angular/core', + }); if (!config.name) { - files.services?.addImport('HttpClient', '@angular/common/http'); + files.services?.addImport({ + imports: 'HttpClient', + module: '@angular/common/http', + }); } - files.services?.addImport({ asType: true, name: 'Observable' }, 'rxjs'); + files.services?.addImport({ + imports: { asType: true, name: 'Observable' }, + module: 'rxjs', + }); } else { - files.services?.addImport( - { asType: true, name: 'CancelablePromise' }, - './core/CancelablePromise', - ); + files.services?.addImport({ + imports: { asType: true, name: 'CancelablePromise' }, + module: './core/CancelablePromise', + }); } if (config.services.response === 'response') { - files.services?.addImport( - { asType: true, name: 'ApiResult' }, - './core/ApiResult', - ); + files.services?.addImport({ + imports: { asType: true, name: 'ApiResult' }, + module: './core/ApiResult', + }); } if (config.name) { - files.services?.addImport( - { asType: config.client !== 'angular', name: 'BaseHttpRequest' }, - './core/BaseHttpRequest', - ); + files.services?.addImport({ + imports: { + asType: config.client.name !== 'angular', + name: 'BaseHttpRequest', + }, + module: './core/BaseHttpRequest', + }); } else { - files.services?.addImport('OpenAPI', './core/OpenAPI'); - files.services?.addImport( - { alias: '__request', name: 'request' }, - './core/request', - ); + files.services?.addImport({ + imports: 'OpenAPI', + module: './core/OpenAPI', + }); + files.services?.addImport({ + imports: { alias: '__request', name: 'request' }, + module: './core/request', + }); } } @@ -741,10 +756,10 @@ export const generateServices = async ({ name, })); if (importedTypes.length) { - files.services?.addImport( - importedTypes, - `./${files.types.getName(false)}`, - ); + files.services?.addImport({ + imports: importedTypes, + module: `./${files.types.getName(false)}`, + }); } } }; diff --git a/packages/openapi-ts/src/generate/utils.ts b/packages/openapi-ts/src/generate/utils.ts new file mode 100644 index 000000000..7697da0c9 --- /dev/null +++ b/packages/openapi-ts/src/generate/utils.ts @@ -0,0 +1,7 @@ +import { existsSync, mkdirSync } from 'node:fs'; + +export const ensureDirSync = (path: string) => { + if (!existsSync(path)) { + mkdirSync(path, { recursive: true }); + } +}; diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index da67a666e..d40db3d12 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -80,7 +80,7 @@ const processOutput = () => { const logClientMessage = () => { const { client } = getConfig(); - switch (client) { + switch (client.name) { case 'angular': return console.log('✨ Creating Angular client'); case '@hey-api/client-axios': @@ -96,6 +96,22 @@ const logClientMessage = () => { } }; +const getClient = (userConfig: ClientConfig): Config['client'] => { + let client: Config['client'] = { + inline: false, + name: 'fetch', + }; + if (typeof userConfig.client === 'string') { + client.name = userConfig.client; + } else { + client = { + ...client, + ...userConfig.client, + }; + } + return client; +}; + const getOutput = (userConfig: ClientConfig): Config['output'] => { let output: Config['output'] = { format: false, @@ -212,7 +228,6 @@ const initConfigs = async (userConfig: UserConfig): Promise => { return userConfigs.map((userConfig) => { const { base, - client = 'fetch', configFile = '', debug = false, dryRun = false, @@ -247,6 +262,7 @@ const initConfigs = async (userConfig: UserConfig): Promise => { ); } + const client = getClient(userConfig); const plugins = getPlugins(userConfig); const schemas = getSchemas(userConfig); const services = getServices(userConfig); diff --git a/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts b/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts index 8951d9b4c..6d81e6ed9 100644 --- a/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts +++ b/packages/openapi-ts/src/openApi/common/parser/__tests__/operation.spec.ts @@ -5,7 +5,9 @@ import { getOperationName, parseResponseStatusCode } from '../operation'; describe('getOperationName', () => { const options1: Parameters[0] = { - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: true, @@ -30,7 +32,9 @@ describe('getOperationName', () => { }; const options2: Parameters[0] = { - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: true, diff --git a/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts b/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts index c8c58953b..bdfe236d7 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/__tests__/getServices.spec.ts @@ -6,7 +6,9 @@ import { getServices } from '../getServices'; describe('getServices', () => { it('should create a unnamed service if tags are empty', () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: true, diff --git a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts index b3fe09250..5ab0c76e0 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getServices.spec.ts @@ -6,7 +6,9 @@ import { getServices } from '../getServices'; describe('getServices', () => { it('should create a unnamed service if tags are empty', () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: true, diff --git a/packages/openapi-ts/src/templates/client.hbs b/packages/openapi-ts/src/templates/client.hbs index ad761b8f4..9f06bfe0c 100644 --- a/packages/openapi-ts/src/templates/client.hbs +++ b/packages/openapi-ts/src/templates/client.hbs @@ -1,4 +1,4 @@ -{{#equals @root.$config.client 'angular'}} +{{#equals @root.$config.client.name 'angular'}} import { NgModule} from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; @@ -20,7 +20,7 @@ import { {{{transformServiceName name}}} } from './services.gen'; {{/each}} {{/if}} -{{#equals @root.$config.client 'angular'}} +{{#equals @root.$config.client.name 'angular'}} @NgModule({ imports: [HttpClientModule], providers: [ diff --git a/packages/openapi-ts/src/templates/core/BaseHttpRequest.hbs b/packages/openapi-ts/src/templates/core/BaseHttpRequest.hbs index f52a645bd..992e94dc8 100644 --- a/packages/openapi-ts/src/templates/core/BaseHttpRequest.hbs +++ b/packages/openapi-ts/src/templates/core/BaseHttpRequest.hbs @@ -1,4 +1,4 @@ -{{#equals @root.$config.client 'angular'}} +{{#equals @root.$config.client.name 'angular'}} import type { HttpClient } from '@angular/common/http'; import type { Observable } from 'rxjs'; @@ -12,7 +12,7 @@ import type { OpenAPIConfig } from './OpenAPI'; export abstract class BaseHttpRequest { - {{#equals @root.$config.client 'angular'}} + {{#equals @root.$config.client.name 'angular'}} constructor( public readonly config: OpenAPIConfig, public readonly http: HttpClient, @@ -21,7 +21,7 @@ export abstract class BaseHttpRequest { constructor(public readonly config: OpenAPIConfig) {} {{/equals}} - {{#equals @root.$config.client 'angular'}} + {{#equals @root.$config.client.name 'angular'}} public abstract request(options: ApiRequestOptions): Observable; {{else}} public abstract request(options: ApiRequestOptions): CancelablePromise; diff --git a/packages/openapi-ts/src/templates/core/HttpRequest.hbs b/packages/openapi-ts/src/templates/core/HttpRequest.hbs index 86afd518a..2ab3f755b 100644 --- a/packages/openapi-ts/src/templates/core/HttpRequest.hbs +++ b/packages/openapi-ts/src/templates/core/HttpRequest.hbs @@ -1,4 +1,4 @@ -{{#equals @root.$config.client 'angular'}} +{{#equals @root.$config.client.name 'angular'}} import { Inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import type { Observable } from 'rxjs'; @@ -16,12 +16,12 @@ import type { OpenAPIConfig } from './OpenAPI'; import { request as __request } from './request'; {{/equals}} -{{#equals @root.$config.client 'angular'}} +{{#equals @root.$config.client.name 'angular'}} @Injectable() {{/equals}} export class {{httpRequest}} extends BaseHttpRequest { - {{#equals @root.$config.client 'angular'}} + {{#equals @root.$config.client.name 'angular'}} constructor( @Inject(OpenAPI) config: OpenAPIConfig, @@ -35,7 +35,7 @@ export class {{httpRequest}} extends BaseHttpRequest { } {{/equals}} - {{#equals @root.$config.client 'angular'}} + {{#equals @root.$config.client.name 'angular'}} /** * Request method * @param options The request options from the service diff --git a/packages/openapi-ts/src/templates/core/OpenAPI.hbs b/packages/openapi-ts/src/templates/core/OpenAPI.hbs index 20fa7bc21..0fd880100 100644 --- a/packages/openapi-ts/src/templates/core/OpenAPI.hbs +++ b/packages/openapi-ts/src/templates/core/OpenAPI.hbs @@ -1,10 +1,10 @@ -{{#equals @root.$config.client 'angular'}} +{{#equals @root.$config.client.name 'angular'}} import type { HttpResponse } from '@angular/common/http'; {{/equals}} -{{#equals @root.$config.client 'axios'}} +{{#equals @root.$config.client.name 'axios'}} import type { AxiosRequestConfig, AxiosResponse } from 'axios'; {{/equals}} -{{#equals @root.$config.client 'node'}} +{{#equals @root.$config.client.name 'node'}} import type { RequestInit, Response } from 'node-fetch'; {{/equals}} import type { ApiRequestOptions } from './ApiRequestOptions'; @@ -43,22 +43,22 @@ export type OpenAPIConfig = { VERSION: string; WITH_CREDENTIALS: boolean; interceptors: { - {{#equals @root.$config.client 'angular'}} + {{#equals @root.$config.client.name 'angular'}} response: Interceptors>; {{/equals}} - {{#equals @root.$config.client 'axios'}} + {{#equals @root.$config.client.name 'axios'}} request: Interceptors; response: Interceptors; {{/equals}} - {{#equals @root.$config.client 'fetch'}} + {{#equals @root.$config.client.name 'fetch'}} request: Interceptors; response: Interceptors; {{/equals}} - {{#equals @root.$config.client 'node'}} + {{#equals @root.$config.client.name 'node'}} request: Interceptors; response: Interceptors; {{/equals}} - {{#equals @root.$config.client 'xhr'}} + {{#equals @root.$config.client.name 'xhr'}} request: Interceptors; response: Interceptors; {{/equals}} @@ -76,7 +76,7 @@ export const OpenAPI: OpenAPIConfig = { VERSION: '{{{version}}}', WITH_CREDENTIALS: false, interceptors: { - {{#notEquals @root.$config.client 'angular'}} + {{#notEquals @root.$config.client.name 'angular'}} request: new Interceptors(), {{/notEquals}} response: new Interceptors(), diff --git a/packages/openapi-ts/src/templates/core/fetch/request.hbs b/packages/openapi-ts/src/templates/core/fetch/request.hbs index 51149db42..4e564d573 100644 --- a/packages/openapi-ts/src/templates/core/fetch/request.hbs +++ b/packages/openapi-ts/src/templates/core/fetch/request.hbs @@ -1,4 +1,4 @@ -{{#equals @root.$config.client 'node'}} +{{#equals @root.$config.client.name 'node'}} import fetch, { FormData, Headers } from 'node-fetch'; import type { RequestInit, Response } from 'node-fetch'; diff --git a/packages/openapi-ts/src/templates/core/fetch/sendRequest.hbs b/packages/openapi-ts/src/templates/core/fetch/sendRequest.hbs index 713460a80..dc9982358 100644 --- a/packages/openapi-ts/src/templates/core/fetch/sendRequest.hbs +++ b/packages/openapi-ts/src/templates/core/fetch/sendRequest.hbs @@ -16,7 +16,7 @@ export const sendRequest = async ( signal: controller.signal, }; - {{#equals @root.$config.client 'fetch'}} + {{#equals @root.$config.client.name 'fetch'}} if (config.WITH_CREDENTIALS) { request.credentials = config.CREDENTIALS; } diff --git a/packages/openapi-ts/src/templates/core/request.hbs b/packages/openapi-ts/src/templates/core/request.hbs index 2dfd9bb55..5f7ec4a1e 100644 --- a/packages/openapi-ts/src/templates/core/request.hbs +++ b/packages/openapi-ts/src/templates/core/request.hbs @@ -1,5 +1,5 @@ -{{~#equals @root.$config.client 'angular'}}{{>angular/request}}{{/equals~}} -{{~#equals @root.$config.client 'axios'}}{{>axios/request}}{{/equals~}} -{{~#equals @root.$config.client 'fetch'}}{{>fetch/request}}{{/equals~}} -{{~#equals @root.$config.client 'node'}}{{>fetch/request}}{{/equals~}} -{{~#equals @root.$config.client 'xhr'}}{{>xhr/request}}{{/equals~}} +{{~#equals @root.$config.client.name 'angular'}}{{>angular/request}}{{/equals~}} +{{~#equals @root.$config.client.name 'axios'}}{{>axios/request}}{{/equals~}} +{{~#equals @root.$config.client.name 'fetch'}}{{>fetch/request}}{{/equals~}} +{{~#equals @root.$config.client.name 'node'}}{{>fetch/request}}{{/equals~}} +{{~#equals @root.$config.client.name 'xhr'}}{{>xhr/request}}{{/equals~}} diff --git a/packages/openapi-ts/src/types/config.ts b/packages/openapi-ts/src/types/config.ts index 3bcc0eb99..91b236b72 100644 --- a/packages/openapi-ts/src/types/config.ts +++ b/packages/openapi-ts/src/types/config.ts @@ -2,6 +2,15 @@ import type { Operation } from '../openApi'; import type { Plugins } from './plugins'; import type { ExtractArrayOfObjects } from './utils'; +type Client = + | '@hey-api/client-axios' + | '@hey-api/client-fetch' + | 'angular' + | 'axios' + | 'fetch' + | 'node' + | 'xhr'; + export interface ClientConfig { /** * Manually set base in OpenAPI config instead of inferring from server value @@ -13,13 +22,24 @@ export interface ClientConfig { * @default 'fetch' */ client?: - | '@hey-api/client-axios' - | '@hey-api/client-fetch' - | 'angular' - | 'axios' - | 'fetch' - | 'node' - | 'xhr'; + | Client + | { + /** + * Inline the client module? Set this to true if you're using a standalone + * client package and don't want to declare it as a separate dependency. + * When true, the client module will be generated from the standalone + * package and bundled with the rest of the generated output. This is + * useful if you're repackaging the output, publishing it to other users, + * and you don't want them to install any dependencies. + * @default false + */ + inline?: boolean; + /** + * HTTP client to generate + * @default 'fetch' + */ + name: Client; + }; /** * Path to the config file. Set this value if you don't use the default * config file name, or it's not located in the project root. @@ -214,6 +234,7 @@ export type UserConfig = ClientConfig; export type Config = Omit< Required, | 'base' + | 'client' | 'name' | 'output' | 'plugins' @@ -223,6 +244,7 @@ export type Config = Omit< | 'types' > & Pick & { + client: Extract['client'], object>; output: Extract['output'], object>; plugins: ExtractArrayOfObjects< Required['plugins'], diff --git a/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts b/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts index 187429467..54bcad6fa 100644 --- a/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts +++ b/packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts @@ -10,7 +10,9 @@ import { describe('registerHandlebarHelpers', () => { it('should register the helpers', () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, @@ -41,7 +43,9 @@ describe('registerHandlebarHelpers', () => { describe('registerHandlebarTemplates', () => { it('should return correct templates', () => { setConfig({ - client: 'fetch', + client: { + name: 'fetch', + }, configFile: '', debug: false, dryRun: false, diff --git a/packages/openapi-ts/src/utils/config.ts b/packages/openapi-ts/src/utils/config.ts index 492dbbb07..cd9cf4c9f 100644 --- a/packages/openapi-ts/src/utils/config.ts +++ b/packages/openapi-ts/src/utils/config.ts @@ -10,6 +10,6 @@ export const setConfig = (config: Config) => { }; export const isStandaloneClient = (config: Config | Config['client']) => { - const client = typeof config === 'string' ? config : config.client; + const client = 'client' in config ? config.client.name : config.name; return client.startsWith('@hey-api'); }; diff --git a/packages/openapi-ts/src/utils/getHttpRequestName.ts b/packages/openapi-ts/src/utils/getHttpRequestName.ts index 4b627f4c7..802df5704 100644 --- a/packages/openapi-ts/src/utils/getHttpRequestName.ts +++ b/packages/openapi-ts/src/utils/getHttpRequestName.ts @@ -5,7 +5,7 @@ import type { Config } from '../types/config'; * @param client HTTP client to generate */ export const getHttpRequestName = (client: Config['client']): string => { - switch (client) { + switch (client.name) { case 'angular': return 'AngularHttpRequest'; case 'axios': diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/client.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/client.ts.snap new file mode 100644 index 000000000..ddf1b548b --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/client.ts.snap @@ -0,0 +1,7 @@ +export { client, createClient } from './core/'; +export type { Client, Options, RequestResult } from './core/types'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from './core/utils'; diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/index.ts.snap new file mode 100644 index 000000000..4db32ef16 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/index.ts.snap @@ -0,0 +1,163 @@ +import type { Client, Config, RequestOptions } from './types'; +import { + createDefaultConfig, + createInterceptors, + createQuerySerializer, + getParseAs, + getUrl, + mergeHeaders, +} from './utils'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +let globalConfig = createDefaultConfig(); + +const globalInterceptors = createInterceptors< + Request, + Response, + RequestOptions +>(); + +export const createClient = (config: Config): Client => { + const defaultConfig = createDefaultConfig(); + const _config = { ...defaultConfig, ...config }; + + if (_config.baseUrl?.endsWith('/')) { + _config.baseUrl = _config.baseUrl.substring(0, _config.baseUrl.length - 1); + } + _config.headers = mergeHeaders(defaultConfig.headers, _config.headers); + + if (_config.global) { + globalConfig = { ..._config }; + } + + // @ts-ignore + const getConfig = () => (_config.root ? globalConfig : _config); + + const interceptors = _config.global + ? globalInterceptors + : createInterceptors(); + + // @ts-ignore + const request: Client['request'] = async (options) => { + const config = getConfig(); + + const opts: RequestOptions = { + ...config, + ...options, + headers: mergeHeaders(config.headers, options.headers), + }; + if (opts.body && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body); + } + + const url = getUrl({ + baseUrl: opts.baseUrl ?? '', + path: opts.path, + query: opts.query, + querySerializer: + typeof opts.querySerializer === 'function' + ? opts.querySerializer + : createQuerySerializer(opts.querySerializer), + url: opts.url, + }); + + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request._fns) { + request = await fn(request, opts); + } + + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response._fns) { + response = await fn(response, request, opts); + } + + const result = { + request, + response, + }; + + // return empty objects so truthy checks for data/error succeed + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + if (response.ok) { + return { + data: {}, + ...result, + }; + } + return { + error: {}, + ...result, + }; + } + + if (response.ok) { + if (opts.parseAs === 'stream') { + return { + data: response.body, + ...result, + }; + } + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + let data = await response[parseAs](); + if (parseAs === 'json' && options.responseTransformer) { + data = await options.responseTransformer(data); + } + + return { + data, + ...result, + }; + } + + let error = await response.text(); + try { + error = JSON.parse(error); + } catch { + // noop + } + return { + error, + ...result, + }; + }; + + return { + connect: (options) => request({ ...options, method: 'CONNECT' }), + delete: (options) => request({ ...options, method: 'DELETE' }), + get: (options) => request({ ...options, method: 'GET' }), + getConfig, + head: (options) => request({ ...options, method: 'HEAD' }), + interceptors, + options: (options) => request({ ...options, method: 'OPTIONS' }), + patch: (options) => request({ ...options, method: 'PATCH' }), + post: (options) => request({ ...options, method: 'POST' }), + put: (options) => request({ ...options, method: 'PUT' }), + request, + trace: (options) => request({ ...options, method: 'TRACE' }), + }; +}; + +export const client = createClient({ + ...globalConfig, + // @ts-ignore + root: true, +}); diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/types.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/types.ts.snap new file mode 100644 index 000000000..13955fb64 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/types.ts.snap @@ -0,0 +1,163 @@ +import type { + BodySerializer, + Middleware, + QuerySerializer, + QuerySerializerOptions, +} from './utils'; + +type OmitKeys = Pick>; + +export interface Config + extends Omit { + /** + * Base URL for all requests made by this client. + * @default '' + */ + baseUrl?: string; + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: + | RequestInit['body'] + | Record + | Array> + | Array + | number; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * @default globalThis.fetch + */ + fetch?: (request: Request) => ReturnType; + /** + * By default, options passed to this call will be applied to the global + * client instance. Set to false to create a local client instance. + * @default true + */ + global?: boolean; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: + | 'CONNECT' + | 'DELETE' + | 'GET' + | 'HEAD' + | 'OPTIONS' + | 'PATCH' + | 'POST' + | 'PUT' + | 'TRACE'; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * @default 'auto' + */ + parseAs?: Exclude | 'auto' | 'stream'; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function for transforming response data before it's returned to the + * caller function. This is an ideal place to post-process server data, + * e.g. convert date ISO strings into native Date objects. + */ + responseTransformer?: (data: unknown) => Promise; +} + +interface RequestOptionsBase extends Omit { + path?: Record; + query?: Record; + url: string; +} + +export type RequestResult = Promise< + ({ data: Data; error: undefined } | { data: undefined; error: Error }) & { + request: Request; + response: Response; + } +>; + +type MethodFn = ( + options: RequestOptionsBase, +) => RequestResult; +type RequestFn = ( + options: RequestOptionsBase & Pick, 'method'>, +) => RequestResult; + +interface ClientBase { + connect: MethodFn; + delete: MethodFn; + get: MethodFn; + getConfig: () => Config; + head: MethodFn; + interceptors: Middleware; + options: MethodFn; + patch: MethodFn; + post: MethodFn; + put: MethodFn; + request: RequestFn; + trace: MethodFn; +} + +export type RequestOptions = RequestOptionsBase & + Config & { + headers: Headers; + }; + +export type Client = ClientBase; + +type OptionsBase = Omit & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; +}; + +export type Options = T extends { body?: any } + ? T extends { headers?: any } + ? OmitKeys & T + : OmitKeys & + T & + Pick + : T extends { headers?: any } + ? OmitKeys & + T & + Pick + : OptionsBase & T; diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/utils.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/utils.ts.snap new file mode 100644 index 000000000..b7f026ce0 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/core/utils.ts.snap @@ -0,0 +1,552 @@ +import type { Config } from './types'; + +interface PathSerializer { + path: Record; + url: string; +} + +const PATH_PARAM_RE = /\{[^{}]+\}/g; + +type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: Record; +}) => { + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + url = url.replace( + match, + style === 'label' ? `.${value as string}` : (value as string), + ); + } + } + return url; +}; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + let search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + search = [ + ...search, + serializeArrayParam({ + allowReserved, + explode: true, + name, + style: 'form', + value, + ...array, + }), + ]; + continue; + } + + if (typeof value === 'object') { + search = [ + ...search, + serializeObjectParam({ + allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...object, + }), + ]; + continue; + } + + search = [ + ...search, + serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }), + ]; + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + content: string | null, +): Exclude => { + if (!content) { + return; + } + + if (content === 'application/json' || content.endsWith('+json')) { + return 'json'; + } + + if (content === 'multipart/form-data') { + return 'formData'; + } + + if ( + [ + 'application/octet-stream', + 'application/pdf', + 'application/zip', + 'audio/', + 'image/', + 'video/', + ].some((type) => content.includes(type)) + ) { + return 'blob'; + } + + if (content.includes('text/')) { + return 'text'; + } +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = baseUrl + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +) => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header || typeof header !== 'object') { + continue; + } + + const iterator = + header instanceof Headers ? header.entries() : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + _fns: Interceptor[]; + + constructor() { + this._fns = []; + } + + eject(fn: Interceptor) { + const index = this._fns.indexOf(fn); + if (index !== -1) { + this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]; + } + } + + use(fn: Interceptor) { + this._fns = [...this._fns, fn]; + } +} + +// `createInterceptors()` response, meant for external use as it does not +// expose internals +export interface Middleware { + request: Pick>, 'eject' | 'use'>; + response: Pick< + Interceptors>, + 'eject' | 'use' + >; +} + +// do not add `Middleware` as return type so we can use _fns internally +export const createInterceptors = () => ({ + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ) => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T) => JSON.stringify(body), +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +) => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ) => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data; + }, +}; + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createDefaultConfig = (): Config => ({ + ...jsonBodySerializer, + baseUrl: '', + fetch: globalThis.fetch, + global: true, + headers: defaultHeaders, + querySerializer: defaultQuerySerializer, +}); diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/index.ts.snap new file mode 100644 index 000000000..0a2b84bae --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/index.ts.snap @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './schemas.gen'; +export * from './services.gen'; +export * from './types.gen'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/schemas.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/schemas.gen.ts.snap new file mode 100644 index 000000000..2f090254b --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/schemas.gen.ts.snap @@ -0,0 +1,1766 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export const $camelCaseCommentWithBreaks = { + description: `Testing multiline comments in string: First line +Second line + +Fourth line`, + type: 'integer' +} as const; + +export const $CommentWithBreaks = { + description: `Testing multiline comments in string: First line +Second line + +Fourth line`, + type: 'integer' +} as const; + +export const $CommentWithBackticks = { + description: 'Testing backticks in string: `backticks` and ```multiple backticks``` should work', + type: 'integer' +} as const; + +export const $CommentWithBackticksAndQuotes = { + description: `Testing backticks and quotes in string: \`backticks\`, 'quotes', "double quotes" and \`\`\`multiple backticks\`\`\` should work`, + type: 'integer' +} as const; + +export const $CommentWithSlashes = { + description: 'Testing slashes in string: \\backwards\\\\\\ and /forwards/// should work', + type: 'integer' +} as const; + +export const $CommentWithExpressionPlaceholders = { + description: 'Testing expression placeholders in string: ${expression} should work', + type: 'integer' +} as const; + +export const $CommentWithQuotes = { + description: `Testing quotes in string: 'single quote''' and "double quotes""" should work`, + type: 'integer' +} as const; + +export const $CommentWithReservedCharacters = { + description: 'Testing reserved characters in string: /* inline */ and /** inline **/ should work', + type: 'integer' +} as const; + +export const $SimpleInteger = { + description: 'This is a simple number', + type: 'integer' +} as const; + +export const $SimpleBoolean = { + description: 'This is a simple boolean', + type: 'boolean' +} as const; + +export const $SimpleString = { + description: 'This is a simple string', + type: 'string' +} as const; + +export const $NonAsciiStringæøåÆØÅöôêÊ字符串 = { + description: 'A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串)', + type: 'string' +} as const; + +export const $SimpleFile = { + description: 'This is a simple file', + type: 'file' +} as const; + +export const $SimpleReference = { + description: 'This is a simple reference', + '$ref': '#/components/schemas/ModelWithString' +} as const; + +export const $SimpleStringWithPattern = { + description: 'This is a simple string', + type: 'string', + nullable: true, + maxLength: 64, + pattern: '^[a-zA-Z0-9_]*$' +} as const; + +export const $EnumWithStrings = { + description: 'This is a simple enum with strings', + enum: ['Success', 'Warning', 'Error', "'Single Quote'", '"Double Quotes"', 'Non-ascii: øæåôöØÆÅÔÖ字符串'] +} as const; + +export const $EnumWithReplacedCharacters = { + enum: ["'Single Quote'", '"Double Quotes"', 'øæåôöØÆÅÔÖ字符串', 3.1, ''], + type: 'string' +} as const; + +export const $EnumWithNumbers = { + description: 'This is a simple enum with numbers', + enum: [1, 2, 3, 1.1, 1.2, 1.3, 100, 200, 300, -100, -200, -300, -1.1, -1.2, -1.3], + default: 200 +} as const; + +export const $EnumFromDescription = { + description: 'Success=1,Warning=2,Error=3', + type: 'number' +} as const; + +export const $EnumWithExtensions = { + description: 'This is a simple enum with numbers', + enum: [200, 400, 500], + 'x-enum-varnames': ['CUSTOM_SUCCESS', 'CUSTOM_WARNING', 'CUSTOM_ERROR'], + 'x-enum-descriptions': ['Used when the status of something is successful', 'Used when the status of something has a warning', 'Used when the status of something has an error'] +} as const; + +export const $EnumWithXEnumNames = { + enum: [0, 1, 2], + 'x-enumNames': ['zero', 'one', 'two'] +} as const; + +export const $ArrayWithNumbers = { + description: 'This is a simple array with numbers', + type: 'array', + items: { + type: 'integer' + } +} as const; + +export const $ArrayWithBooleans = { + description: 'This is a simple array with booleans', + type: 'array', + items: { + type: 'boolean' + } +} as const; + +export const $ArrayWithStrings = { + description: 'This is a simple array with strings', + type: 'array', + items: { + type: 'string' + }, + default: ['test'] +} as const; + +export const $ArrayWithReferences = { + description: 'This is a simple array with references', + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithString' + } +} as const; + +export const $ArrayWithArray = { + description: 'This is a simple array containing an array', + type: 'array', + items: { + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithString' + } + } +} as const; + +export const $ArrayWithProperties = { + description: 'This is a simple array with properties', + type: 'array', + items: { + type: 'object', + properties: { + '16x16': { + '$ref': '#/components/schemas/camelCaseCommentWithBreaks' + }, + bar: { + type: 'string' + } + } + } +} as const; + +export const $ArrayWithAnyOfProperties = { + description: 'This is a simple array with any of properties', + type: 'array', + items: { + anyOf: [ + { + type: 'object', + properties: { + foo: { + type: 'string', + default: 'test' + } + } + }, + { + type: 'object', + properties: { + bar: { + type: 'string' + } + } + } + ] + } +} as const; + +export const $AnyOfAnyAndNull = { + type: 'object', + properties: { + data: { + anyOf: [ + {}, + { + type: 'null' + } + ] + } + } +} as const; + +export const $AnyOfArrays = { + description: 'This is a simple array with any of properties', + type: 'object', + properties: { + results: { + items: { + anyOf: [ + { + type: 'object', + properties: { + foo: { + type: 'string' + } + } + }, + { + type: 'object', + properties: { + bar: { + type: 'string' + } + } + } + ] + }, + type: 'array' + } + } +} as const; + +export const $DictionaryWithString = { + description: 'This is a string dictionary', + type: 'object', + additionalProperties: { + type: 'string' + } +} as const; + +export const $DictionaryWithPropertiesAndAdditionalProperties = { + type: 'object', + properties: { + foo: { + type: 'number' + }, + bar: { + type: 'boolean' + } + }, + additionalProperties: { + type: 'string' + } +} as const; + +export const $DictionaryWithReference = { + description: 'This is a string reference', + type: 'object', + additionalProperties: { + '$ref': '#/components/schemas/ModelWithString' + } +} as const; + +export const $DictionaryWithArray = { + description: 'This is a complex dictionary', + type: 'object', + additionalProperties: { + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithString' + } + } +} as const; + +export const $DictionaryWithDictionary = { + description: 'This is a string dictionary', + type: 'object', + additionalProperties: { + type: 'object', + additionalProperties: { + type: 'string' + } + } +} as const; + +export const $DictionaryWithProperties = { + description: 'This is a complex dictionary', + type: 'object', + additionalProperties: { + type: 'object', + properties: { + foo: { + type: 'string' + }, + bar: { + type: 'string' + } + } + } +} as const; + +export const $ModelWithInteger = { + description: 'This is a model with one number property', + type: 'object', + properties: { + prop: { + description: 'This is a simple number property', + type: 'integer' + } + } +} as const; + +export const $ModelWithBoolean = { + description: 'This is a model with one boolean property', + type: 'object', + properties: { + prop: { + description: 'This is a simple boolean property', + type: 'boolean' + } + } +} as const; + +export const $ModelWithString = { + description: 'This is a model with one string property', + type: 'object', + properties: { + prop: { + description: 'This is a simple string property', + type: 'string' + } + } +} as const; + +export const $ModelWithStringError = { + description: 'This is a model with one string property', + type: 'object', + properties: { + prop: { + description: 'This is a simple string property', + type: 'string' + } + } +} as const; + +export const $Model_From_Zendesk = { + description: `\`Comment\` or \`VoiceComment\`. The JSON object for adding voice comments to tickets is different. See [Adding voice comments to tickets](/documentation/ticketing/managing-tickets/adding-voice-comments-to-tickets)`, + type: 'string' +} as const; + +export const $ModelWithNullableString = { + description: 'This is a model with one string property', + type: 'object', + required: ['nullableRequiredProp1', 'nullableRequiredProp2'], + properties: { + nullableProp1: { + description: 'This is a simple string property', + type: 'string', + nullable: true + }, + nullableRequiredProp1: { + description: 'This is a simple string property', + type: 'string', + nullable: true + }, + nullableProp2: { + description: 'This is a simple string property', + type: ['string', 'null'] + }, + nullableRequiredProp2: { + description: 'This is a simple string property', + type: ['string', 'null'] + }, + 'foo_bar-enum': { + description: 'This is a simple enum with strings', + enum: ['Success', 'Warning', 'Error', 'ØÆÅ字符串'] + } + } +} as const; + +export const $ModelWithEnum = { + description: 'This is a model with one enum', + type: 'object', + properties: { + 'foo_bar-enum': { + description: 'This is a simple enum with strings', + enum: ['Success', 'Warning', 'Error', 'ØÆÅ字符串'] + }, + statusCode: { + description: 'These are the HTTP error code enums', + enum: ['100', '200 FOO', '300 FOO_BAR', '400 foo-bar', '500 foo.bar', '600 foo&bar'] + }, + bool: { + description: 'Simple boolean enum', + type: 'boolean', + enum: [true] + } + } +} as const; + +export const $ModelWithEnumWithHyphen = { + description: 'This is a model with one enum with escaped name', + type: 'object', + properties: { + 'foo-bar-baz-qux': { + type: 'string', + enum: ['3.0'], + title: 'Foo-Bar-Baz-Qux', + default: '3.0' + } + } +} as const; + +export const $ModelWithEnumFromDescription = { + description: 'This is a model with one enum', + type: 'object', + properties: { + test: { + type: 'integer', + description: 'Success=1,Warning=2,Error=3' + } + } +} as const; + +export const $ModelWithNestedEnums = { + description: 'This is a model with nested enums', + type: 'object', + properties: { + dictionaryWithEnum: { + type: 'object', + additionalProperties: { + enum: ['Success', 'Warning', 'Error'] + } + }, + dictionaryWithEnumFromDescription: { + type: 'object', + additionalProperties: { + type: 'integer', + description: 'Success=1,Warning=2,Error=3' + } + }, + arrayWithEnum: { + type: 'array', + items: { + enum: ['Success', 'Warning', 'Error'] + } + }, + arrayWithDescription: { + type: 'array', + items: { + type: 'integer', + description: 'Success=1,Warning=2,Error=3' + } + }, + 'foo_bar-enum': { + description: 'This is a simple enum with strings', + enum: ['Success', 'Warning', 'Error', 'ØÆÅ字符串'] + } + } +} as const; + +export const $ModelWithReference = { + description: 'This is a model with one property containing a reference', + type: 'object', + properties: { + prop: { + '$ref': '#/components/schemas/ModelWithProperties' + } + } +} as const; + +export const $ModelWithArrayReadOnlyAndWriteOnly = { + description: 'This is a model with one property containing an array', + type: 'object', + properties: { + prop: { + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithReadOnlyAndWriteOnly' + } + }, + propWithFile: { + type: 'array', + items: { + type: 'file' + } + }, + propWithNumber: { + type: 'array', + items: { + type: 'number' + } + } + } +} as const; + +export const $ModelWithArray = { + description: 'This is a model with one property containing an array', + type: 'object', + properties: { + prop: { + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithString' + } + }, + propWithFile: { + type: 'array', + items: { + type: 'file' + } + }, + propWithNumber: { + type: 'array', + items: { + type: 'number' + } + } + } +} as const; + +export const $ModelWithDictionary = { + description: 'This is a model with one property containing a dictionary', + type: 'object', + properties: { + prop: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + } +} as const; + +export const $DeprecatedModel = { + deprecated: true, + description: 'This is a deprecated model with a deprecated property', + type: 'object', + properties: { + prop: { + deprecated: true, + description: 'This is a deprecated property', + type: 'string' + } + } +} as const; + +export const $ModelWithCircularReference = { + description: 'This is a model with one property containing a circular reference', + type: 'object', + properties: { + prop: { + '$ref': '#/components/schemas/ModelWithCircularReference' + } + } +} as const; + +export const $CompositionWithOneOf = { + description: "This is a model with one property with a 'one of' relationship", + type: 'object', + properties: { + propA: { + type: 'object', + oneOf: [ + { + '$ref': '#/components/schemas/ModelWithString' + }, + { + '$ref': '#/components/schemas/ModelWithEnum' + }, + { + '$ref': '#/components/schemas/ModelWithArray' + }, + { + '$ref': '#/components/schemas/ModelWithDictionary' + } + ] + } + } +} as const; + +export const $CompositionWithOneOfAnonymous = { + description: "This is a model with one property with a 'one of' relationship where the options are not $ref", + type: 'object', + properties: { + propA: { + type: 'object', + oneOf: [ + { + description: 'Anonymous object type', + type: 'object', + properties: { + propA: { + type: 'string' + } + } + }, + { + description: 'Anonymous string type', + type: 'string' + }, + { + description: 'Anonymous integer type', + type: 'integer' + } + ] + } + } +} as const; + +export const $ModelCircle = { + description: 'Circle', + type: 'object', + required: ['kind'], + properties: { + kind: { + type: 'string' + }, + radius: { + type: 'number' + } + } +} as const; + +export const $ModelSquare = { + description: 'Square', + type: 'object', + required: ['kind'], + properties: { + kind: { + type: 'string' + }, + sideLength: { + type: 'number' + } + } +} as const; + +export const $CompositionWithOneOfDiscriminator = { + description: "This is a model with one property with a 'one of' relationship where the options are not $ref", + type: 'object', + oneOf: [ + { + '$ref': '#/components/schemas/ModelCircle' + }, + { + '$ref': '#/components/schemas/ModelSquare' + } + ], + discriminator: { + propertyName: 'kind', + mapping: { + circle: '#/components/schemas/ModelCircle', + square: '#/components/schemas/ModelSquare' + } + } +} as const; + +export const $CompositionWithAnyOf = { + description: "This is a model with one property with a 'any of' relationship", + type: 'object', + properties: { + propA: { + type: 'object', + anyOf: [ + { + '$ref': '#/components/schemas/ModelWithString' + }, + { + '$ref': '#/components/schemas/ModelWithEnum' + }, + { + '$ref': '#/components/schemas/ModelWithArray' + }, + { + '$ref': '#/components/schemas/ModelWithDictionary' + } + ] + } + } +} as const; + +export const $CompositionWithAnyOfAnonymous = { + description: "This is a model with one property with a 'any of' relationship where the options are not $ref", + type: 'object', + properties: { + propA: { + type: 'object', + anyOf: [ + { + description: 'Anonymous object type', + type: 'object', + properties: { + propA: { + type: 'string' + } + } + }, + { + description: 'Anonymous string type', + type: 'string' + }, + { + description: 'Anonymous integer type', + type: 'integer' + } + ] + } + } +} as const; + +export const $CompositionWithNestedAnyAndTypeNull = { + description: "This is a model with nested 'any of' property with a type null", + type: 'object', + properties: { + propA: { + type: 'object', + anyOf: [ + { + items: { + anyOf: [ + { + '$ref': '#/components/schemas/ModelWithDictionary' + }, + { + type: 'null' + } + ] + }, + type: 'array' + }, + { + items: { + anyOf: [ + { + '$ref': '#/components/schemas/ModelWithArray' + }, + { + type: 'null' + } + ] + }, + type: 'array' + } + ] + } + } +} as const; + +export const $e_num_1Период = { + enum: ['Bird', 'Dog'], + type: 'string' +} as const; + +export const $ConstValue = { + type: 'string', + const: 'ConstValue' +} as const; + +export const $CompositionWithNestedAnyOfAndNull = { + description: "This is a model with one property with a 'any of' relationship where the options are not $ref", + type: 'object', + properties: { + propA: { + anyOf: [ + { + items: { + anyOf: [ + { + '$ref': '#/components/schemas/3e-num_1Период' + }, + { + '$ref': '#/components/schemas/ConstValue' + } + ] + }, + type: 'array' + }, + { + type: 'null' + } + ], + title: 'Scopes' + } + } +} as const; + +export const $CompositionWithOneOfAndNullable = { + description: "This is a model with one property with a 'one of' relationship", + type: 'object', + properties: { + propA: { + nullable: true, + type: 'object', + oneOf: [ + { + type: 'object', + properties: { + boolean: { + type: 'boolean' + } + } + }, + { + '$ref': '#/components/schemas/ModelWithEnum' + }, + { + '$ref': '#/components/schemas/ModelWithArray' + }, + { + '$ref': '#/components/schemas/ModelWithDictionary' + } + ] + } + } +} as const; + +export const $CompositionWithOneOfAndSimpleDictionary = { + description: 'This is a model that contains a simple dictionary within composition', + type: 'object', + properties: { + propA: { + oneOf: [ + { + type: 'boolean' + }, + { + type: 'object', + additionalProperties: { + type: 'number' + } + } + ] + } + } +} as const; + +export const $CompositionWithOneOfAndSimpleArrayDictionary = { + description: 'This is a model that contains a dictionary of simple arrays within composition', + type: 'object', + properties: { + propA: { + oneOf: [ + { + type: 'boolean' + }, + { + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'boolean' + } + } + } + ] + } + } +} as const; + +export const $CompositionWithOneOfAndComplexArrayDictionary = { + description: 'This is a model that contains a dictionary of complex arrays (composited) within composition', + type: 'object', + properties: { + propA: { + oneOf: [ + { + type: 'boolean' + }, + { + type: 'object', + additionalProperties: { + type: 'array', + items: { + oneOf: [ + { + type: 'number' + }, + { + type: 'string' + } + ] + } + } + } + ] + } + } +} as const; + +export const $CompositionWithAllOfAndNullable = { + description: "This is a model with one property with a 'all of' relationship", + type: 'object', + properties: { + propA: { + nullable: true, + type: 'object', + allOf: [ + { + type: 'object', + properties: { + boolean: { + type: 'boolean' + } + } + }, + { + '$ref': '#/components/schemas/ModelWithEnum' + }, + { + '$ref': '#/components/schemas/ModelWithArray' + }, + { + '$ref': '#/components/schemas/ModelWithDictionary' + } + ] + } + } +} as const; + +export const $CompositionWithAnyOfAndNullable = { + description: "This is a model with one property with a 'any of' relationship", + type: 'object', + properties: { + propA: { + nullable: true, + type: 'object', + anyOf: [ + { + type: 'object', + properties: { + boolean: { + type: 'boolean' + } + } + }, + { + '$ref': '#/components/schemas/ModelWithEnum' + }, + { + '$ref': '#/components/schemas/ModelWithArray' + }, + { + '$ref': '#/components/schemas/ModelWithDictionary' + } + ] + } + } +} as const; + +export const $CompositionBaseModel = { + description: 'This is a base model with two simple optional properties', + type: 'object', + properties: { + firstName: { + type: 'string' + }, + lastname: { + type: 'string' + } + } +} as const; + +export const $CompositionExtendedModel = { + description: 'This is a model that extends the base model', + type: 'object', + allOf: [ + { + '$ref': '#/components/schemas/CompositionBaseModel' + } + ], + properties: { + age: { + type: 'number' + } + }, + required: ['firstName', 'lastname', 'age'] +} as const; + +export const $ModelWithProperties = { + description: 'This is a model with one nested property', + type: 'object', + required: ['required', 'requiredAndReadOnly', 'requiredAndNullable'], + properties: { + required: { + type: 'string' + }, + requiredAndReadOnly: { + type: 'string', + readOnly: true + }, + requiredAndNullable: { + type: 'string', + nullable: true + }, + string: { + type: 'string' + }, + number: { + type: 'number' + }, + boolean: { + type: 'boolean' + }, + reference: { + '$ref': '#/components/schemas/ModelWithString' + }, + 'property with space': { + type: 'string' + }, + default: { + type: 'string' + }, + try: { + type: 'string' + }, + '@namespace.string': { + type: 'string', + readOnly: true + }, + '@namespace.integer': { + type: 'integer', + readOnly: true + } + } +} as const; + +export const $ModelWithNestedProperties = { + description: 'This is a model with one nested property', + type: 'object', + required: ['first'], + properties: { + first: { + type: 'object', + required: ['second'], + readOnly: true, + nullable: true, + properties: { + second: { + type: 'object', + required: ['third'], + readOnly: true, + nullable: true, + properties: { + third: { + type: 'string', + required: true, + readOnly: true, + nullable: true + } + } + } + } + } + } +} as const; + +export const $ModelWithDuplicateProperties = { + description: 'This is a model with duplicated properties', + type: 'object', + properties: { + prop: { + '$ref': '#/components/schemas/ModelWithString' + } + } +} as const; + +export const $ModelWithOrderedProperties = { + description: 'This is a model with ordered properties', + type: 'object', + properties: { + zebra: { + type: 'string' + }, + apple: { + type: 'string' + }, + hawaii: { + type: 'string' + } + } +} as const; + +export const $ModelWithDuplicateImports = { + description: 'This is a model with duplicated imports', + type: 'object', + properties: { + propA: { + '$ref': '#/components/schemas/ModelWithString' + }, + propB: { + '$ref': '#/components/schemas/ModelWithString' + }, + propC: { + '$ref': '#/components/schemas/ModelWithString' + } + } +} as const; + +export const $ModelThatExtends = { + description: 'This is a model that extends another model', + type: 'object', + allOf: [ + { + '$ref': '#/components/schemas/ModelWithString' + }, + { + type: 'object', + properties: { + propExtendsA: { + type: 'string' + }, + propExtendsB: { + '$ref': '#/components/schemas/ModelWithString' + } + } + } + ] +} as const; + +export const $ModelThatExtendsExtends = { + description: 'This is a model that extends another model', + type: 'object', + allOf: [ + { + '$ref': '#/components/schemas/ModelWithString' + }, + { + '$ref': '#/components/schemas/ModelThatExtends' + }, + { + type: 'object', + properties: { + propExtendsC: { + type: 'string' + }, + propExtendsD: { + '$ref': '#/components/schemas/ModelWithString' + } + } + } + ] +} as const; + +export const $ModelWithPattern = { + description: 'This is a model that contains a some patterns', + type: 'object', + required: ['key', 'name'], + properties: { + key: { + maxLength: 64, + pattern: '^[a-zA-Z0-9_]*$', + type: 'string' + }, + name: { + maxLength: 255, + type: 'string' + }, + enabled: { + type: 'boolean', + readOnly: true + }, + modified: { + type: 'string', + format: 'date-time', + readOnly: true + }, + id: { + type: 'string', + pattern: '^\\d{2}-\\d{3}-\\d{4}$' + }, + text: { + type: 'string', + pattern: '^\\w+$' + }, + patternWithSingleQuotes: { + type: 'string', + pattern: "^[a-zA-Z0-9']*$" + }, + patternWithNewline: { + type: 'string', + pattern: `aaa +bbb` + }, + patternWithBacktick: { + type: 'string', + pattern: 'aaa`bbb' + } + } +} as const; + +export const $File = { + required: ['mime'], + type: 'object', + properties: { + id: { + title: 'Id', + type: 'string', + readOnly: true, + minLength: 1 + }, + updated_at: { + title: 'Updated at', + type: 'string', + format: 'date-time', + readOnly: true + }, + created_at: { + title: 'Created at', + type: 'string', + format: 'date-time', + readOnly: true + }, + mime: { + title: 'Mime', + type: 'string', + maxLength: 24, + minLength: 1 + }, + file: { + title: 'File', + type: 'string', + readOnly: true, + format: 'uri' + } + } +} as const; + +export const $default = { + type: 'object', + properties: { + name: { + type: 'string' + } + } +} as const; + +export const $Pageable = { + type: 'object', + properties: { + page: { + minimum: 0, + type: 'integer', + format: 'int32', + default: 0 + }, + size: { + minimum: 1, + type: 'integer', + format: 'int32' + }, + sort: { + type: 'array', + items: { + type: 'string' + } + } + } +} as const; + +export const $FreeFormObjectWithoutAdditionalProperties = { + description: 'This is a free-form object without additionalProperties.', + type: 'object' +} as const; + +export const $FreeFormObjectWithAdditionalPropertiesEqTrue = { + description: 'This is a free-form object with additionalProperties: true.', + type: 'object', + additionalProperties: true +} as const; + +export const $FreeFormObjectWithAdditionalPropertiesEqEmptyObject = { + description: 'This is a free-form object with additionalProperties: {}.', + type: 'object', + additionalProperties: {} +} as const; + +export const $ModelWithConst = { + type: 'object', + properties: { + String: { + const: 'String' + }, + number: { + const: 0 + }, + null: { + const: null + }, + withType: { + type: 'string', + const: 'Some string' + } + } +} as const; + +export const $ModelWithAdditionalPropertiesEqTrue = { + description: 'This is a model with one property and additionalProperties: true', + type: 'object', + properties: { + prop: { + description: 'This is a simple string property', + type: 'string' + } + }, + additionalProperties: true +} as const; + +export const $NestedAnyOfArraysNullable = { + properties: { + nullableArray: { + anyOf: [ + { + items: { + anyOf: [ + { + type: 'string' + }, + { + type: 'boolean' + } + ] + }, + type: 'array' + }, + { + type: 'null' + } + ] + } + }, + type: 'object' +} as const; + +export const $CompositionWithOneOfAndProperties = { + type: 'object', + oneOf: [ + { + type: 'object', + required: ['foo'], + properties: { + foo: { + '$ref': '#/components/parameters/SimpleParameter' + } + }, + additionalProperties: false + }, + { + type: 'object', + required: ['bar'], + properties: { + bar: { + '$ref': '#/components/schemas/NonAsciiStringæøåÆØÅöôêÊ字符串' + } + }, + additionalProperties: false + } + ], + required: ['baz', 'qux'], + properties: { + baz: { + type: 'integer', + format: 'uint16', + minimum: 0, + nullable: true + }, + qux: { + type: 'integer', + format: 'uint8', + minimum: 0 + } + } +} as const; + +export const $NullableObject = { + type: ['object', 'null'], + description: 'An object that can be null', + properties: { + foo: { + type: 'string' + } + }, + default: null +} as const; + +export const $CharactersInDescription = { + type: 'string', + description: 'Some % character' +} as const; + +export const $ModelWithNullableObject = { + type: 'object', + properties: { + data: { + '$ref': '#/components/schemas/NullableObject' + } + } +} as const; + +export const $ModelWithOneOfEnum = { + oneOf: [ + { + type: 'object', + required: ['foo'], + properties: { + foo: { + type: 'string', + enum: ['Bar'] + } + } + }, + { + type: 'object', + required: ['foo'], + properties: { + foo: { + type: 'string', + enum: ['Baz'] + } + } + }, + { + type: 'object', + required: ['foo'], + properties: { + foo: { + type: 'string', + enum: ['Qux'] + } + } + }, + { + type: 'object', + required: ['content', 'foo'], + properties: { + content: { + type: 'string', + format: 'date-time' + }, + foo: { + type: 'string', + enum: ['Quux'] + } + } + }, + { + type: 'object', + required: ['content', 'foo'], + properties: { + content: { + type: 'array', + items: [ + { + type: 'string', + format: 'date-time' + }, + { + type: 'string' + } + ], + maxItems: 2, + minItems: 2 + }, + foo: { + type: 'string', + enum: ['Corge'] + } + } + } + ] +} as const; + +export const $ModelWithNestedArrayEnumsDataFoo = { + enum: ['foo', 'bar'], + type: 'string' +} as const; + +export const $ModelWithNestedArrayEnumsDataBar = { + enum: ['baz', 'qux'], + type: 'string' +} as const; + +export const $ModelWithNestedArrayEnumsData = { + type: 'object', + properties: { + foo: { + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithNestedArrayEnumsDataFoo' + } + }, + bar: { + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithNestedArrayEnumsDataBar' + } + } + } +} as const; + +export const $ModelWithNestedArrayEnums = { + type: 'object', + properties: { + array_strings: { + type: 'array', + items: { + type: 'string' + } + }, + data: { + allOf: [ + { + '$ref': '#/components/schemas/ModelWithNestedArrayEnumsData' + } + ] + } + } +} as const; + +export const $ModelWithNestedCompositionEnums = { + type: 'object', + properties: { + foo: { + allOf: [ + { + '$ref': '#/components/schemas/ModelWithNestedArrayEnumsDataFoo' + } + ] + } + } +} as const; + +export const $ModelWithReadOnlyAndWriteOnly = { + type: 'object', + required: ['foo', 'bar', 'baz'], + properties: { + foo: { + type: 'string' + }, + bar: { + readOnly: true, + type: 'string' + }, + baz: { + type: 'string', + writeOnly: true + } + } +} as const; + +export const $ModelWithConstantSizeArray = { + type: 'array', + items: { + type: 'number' + }, + minItems: 2, + maxItems: 2 +} as const; + +export const $ModelWithAnyOfConstantSizeArray = { + type: 'array', + items: { + oneOf: [ + { + type: 'number' + }, + { + type: 'string' + } + ] + }, + minItems: 3, + maxItems: 3 +} as const; + +export const $ModelWithPrefixItemsConstantSizeArray = { + type: 'array', + prefixItems: [ + { + '$ref': '#/components/schemas/ModelWithInteger' + }, + { + oneOf: [ + { + type: 'number' + }, + { + type: 'string' + } + ] + }, + { + type: 'string' + } + ] +} as const; + +export const $ModelWithAnyOfConstantSizeArrayNullable = { + type: ['array'], + items: { + oneOf: [ + { + type: 'number', + nullable: true + }, + { + type: 'string' + } + ] + }, + minItems: 3, + maxItems: 3 +} as const; + +export const $ModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = { + type: 'array', + items: { + oneOf: [ + { + type: 'number' + }, + { + '$ref': '#/components/schemas/import' + } + ] + }, + minItems: 2, + maxItems: 2 +} as const; + +export const $ModelWithAnyOfConstantSizeArrayAndIntersect = { + type: 'array', + items: { + allOf: [ + { + type: 'number' + }, + { + type: 'string' + } + ] + }, + minItems: 2, + maxItems: 2 +} as const; + +export const $ModelWithNumericEnumUnion = { + type: 'object', + properties: { + value: { + type: 'number', + description: 'Период', + enum: [-10, -1, 0, 1, 3, 6, 12] + } + } +} as const; + +export const $ModelWithBackticksInDescription = { + description: 'Some description with `back ticks`', + type: 'object', + properties: { + template: { + type: 'string', + description: `The template \`that\` should be used for parsing and importing the contents of the CSV file. + +

There is one placeholder currently supported:

  • \${x} - refers to the n-th column in the CSV file, e.g. \${1}, \${2}, ...)

Example of a correct JSON template:

+
+[
+  {
+    "resourceType": "Asset",
+    "identifier": {
+      "name": "\${1}",
+      "domain": {
+        "name": "\${2}",
+        "community": {
+          "name": "Some Community"
+        }
+      }
+    },
+    "attributes" : {
+      "00000000-0000-0000-0000-000000003115" : [ {
+        "value" : "\${3}" 
+      } ],
+      "00000000-0000-0000-0000-000000000222" : [ {
+        "value" : "\${4}"
+      } ]
+    }
+  }
+]
+
` + } + } +} as const; + +export const $ModelWithOneOfAndProperties = { + type: 'object', + oneOf: [ + { + '$ref': '#/components/parameters/SimpleParameter' + }, + { + '$ref': '#/components/schemas/NonAsciiStringæøåÆØÅöôêÊ字符串' + } + ], + required: ['baz', 'qux'], + properties: { + baz: { + type: 'integer', + format: 'uint16', + minimum: 0, + nullable: true + }, + qux: { + type: 'integer', + format: 'uint8', + minimum: 0 + } + } +} as const; + +export const $ParameterSimpleParameterUnused = { + description: 'Model used to test deduplication strategy (unused)', + type: 'string' +} as const; + +export const $PostServiceWithEmptyTagResponse = { + description: 'Model used to test deduplication strategy', + type: 'string' +} as const; + +export const $PostServiceWithEmptyTagResponse2 = { + description: 'Model used to test deduplication strategy', + type: 'string' +} as const; + +export const $DeleteFooData = { + description: 'Model used to test deduplication strategy', + type: 'string' +} as const; + +export const $DeleteFooData2 = { + description: 'Model used to test deduplication strategy', + type: 'string' +} as const; + +export const $import = { + description: 'Model with restricted keyword name', + type: 'string' +} as const; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/services.gen.ts.snap new file mode 100644 index 000000000..7adfc6d1d --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/services.gen.ts.snap @@ -0,0 +1,241 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { client, type Options, formDataBodySerializer, urlSearchParamsBodySerializer } from './client'; +import type { ImportData, ImportError, ImportResponse, ApiVversionOdataControllerCountError, ApiVversionOdataControllerCountResponse, DeleteFooData3, CallWithDescriptionsData, DeprecatedCallData, CallWithParametersData, CallWithWeirdParameterNamesData, GetCallWithOptionalParamData, PostCallWithOptionalParamData, PostApiRequestBodyData, PostApiFormDataData, CallWithDefaultParametersData, CallWithDefaultOptionalParametersData, CallToTestOrderOfParamsData, CallWithNoContentResponseError, CallWithNoContentResponseResponse, CallWithResponseAndNoContentResponseError, CallWithResponseAndNoContentResponseResponse, DummyAError, DummyAResponse, DummyBError, DummyBResponse, CallWithResponseError, CallWithResponseResponse, CallWithDuplicateResponsesError, CallWithDuplicateResponsesResponse, CallWithResponsesError, CallWithResponsesResponse, CollectionFormatData, TypesData, TypesError, TypesResponse, UploadFileData, UploadFileError, UploadFileResponse, FileResponseData, FileResponseError, FileResponseResponse, ComplexTypesData, ComplexTypesError, ComplexTypesResponse, MultipartRequestData, MultipartResponseError, MultipartResponseResponse, ComplexParamsData, ComplexParamsError, ComplexParamsResponse, CallWithResultFromHeaderError, CallWithResultFromHeaderResponse, TestErrorCodeData, TestErrorCodeError, TestErrorCodeResponse, NonAsciiæøåÆøÅöôêÊ字符串Data, NonAsciiæøåÆøÅöôêÊ字符串Error, NonAsciiæøåÆøÅöôêÊ字符串Response, PutWithFormUrlEncodedData } from './types.gen'; + +export const export_ = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/no-tag' +}); }; + +export const import_ = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/no-tag' +}); }; + +export const apiVVersionOdataControllerCount = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/simple/$count' +}); }; + +export const getCallWithoutParametersAndResponse = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/simple' +}); }; + +export const putCallWithoutParametersAndResponse = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/v{api-version}/simple' +}); }; + +export const postCallWithoutParametersAndResponse = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/simple' +}); }; + +export const deleteCallWithoutParametersAndResponse = (options?: Options) => { return (options?.client ?? client).delete({ + ...options, + url: '/api/v{api-version}/simple' +}); }; + +export const optionsCallWithoutParametersAndResponse = (options?: Options) => { return (options?.client ?? client).options({ + ...options, + url: '/api/v{api-version}/simple' +}); }; + +export const headCallWithoutParametersAndResponse = (options?: Options) => { return (options?.client ?? client).head({ + ...options, + url: '/api/v{api-version}/simple' +}); }; + +export const patchCallWithoutParametersAndResponse = (options?: Options) => { return (options?.client ?? client).patch({ + ...options, + url: '/api/v{api-version}/simple' +}); }; + +export const deleteFoo = (options: Options) => { return (options?.client ?? client).delete({ + ...options, + url: '/api/v{api-version}/foo/{foo_param}/bar/{BarParam}' +}); }; + +export const callWithDescriptions = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/descriptions/' +}); }; + +/** + * @deprecated + */ +export const deprecatedCall = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/parameters/deprecated' +}); }; + +export const callWithParameters = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/parameters/{parameterPath}' +}); }; + +export const callWithWeirdParameterNames = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/parameters/{parameter.path.1}/{parameter-path-2}/{PARAMETER-PATH-3}' +}); }; + +export const getCallWithOptionalParam = (options: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/parameters/' +}); }; + +export const postCallWithOptionalParam = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/parameters/' +}); }; + +export const postApiRequestBody = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/requestBody/' +}); }; + +export const postApiFormData = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + ...formDataBodySerializer, + url: '/api/v{api-version}/formData/' +}); }; + +export const callWithDefaultParameters = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/defaults' +}); }; + +export const callWithDefaultOptionalParameters = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/defaults' +}); }; + +export const callToTestOrderOfParams = (options: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/v{api-version}/defaults' +}); }; + +export const duplicateName = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/duplicate' +}); }; + +export const duplicateName1 = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/duplicate' +}); }; + +export const duplicateName2 = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/v{api-version}/duplicate' +}); }; + +export const duplicateName3 = (options?: Options) => { return (options?.client ?? client).delete({ + ...options, + url: '/api/v{api-version}/duplicate' +}); }; + +export const callWithNoContentResponse = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/no-content' +}); }; + +export const callWithResponseAndNoContentResponse = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/multiple-tags/response-and-no-content' +}); }; + +export const dummyA = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/multiple-tags/a' +}); }; + +export const dummyB = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/multiple-tags/b' +}); }; + +export const callWithResponse = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/response' +}); }; + +export const callWithDuplicateResponses = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/response' +}); }; + +export const callWithResponses = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/v{api-version}/response' +}); }; + +export const collectionFormat = (options: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/collectionFormat' +}); }; + +export const types = (options: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/types' +}); }; + +export const uploadFile = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/upload' +}); }; + +export const fileResponse = (options: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/file/{id}' +}); }; + +export const complexTypes = (options: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/complex' +}); }; + +export const multipartRequest = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + ...formDataBodySerializer, + url: '/api/v{api-version}/multipart' +}); }; + +export const multipartResponse = (options?: Options) => { return (options?.client ?? client).get({ + ...options, + url: '/api/v{api-version}/multipart' +}); }; + +export const complexParams = (options: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/v{api-version}/complex/{id}' +}); }; + +export const callWithResultFromHeader = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/header' +}); }; + +export const testErrorCode = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/error' +}); }; + +export const nonAsciiæøåÆøÅöôêÊ字符串 = (options: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串' +}); }; + +/** + * Login User + */ +export const putWithFormUrlEncoded = (options: Options) => { return (options?.client ?? client).put({ + ...options, + ...urlSearchParamsBodySerializer, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + url: '/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串' +}); }; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/types.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/types.gen.ts.snap new file mode 100644 index 000000000..1a058b0aa --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline/types.gen.ts.snap @@ -0,0 +1,1860 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * Testing multiline comments in string: First line + * Second line + * + * Fourth line + */ +export type camelCaseCommentWithBreaks = number; + +/** + * Testing multiline comments in string: First line + * Second line + * + * Fourth line + */ +export type CommentWithBreaks = number; + +/** + * Testing backticks in string: `backticks` and ```multiple backticks``` should work + */ +export type CommentWithBackticks = number; + +/** + * Testing backticks and quotes in string: `backticks`, 'quotes', "double quotes" and ```multiple backticks``` should work + */ +export type CommentWithBackticksAndQuotes = number; + +/** + * Testing slashes in string: \backwards\\\ and /forwards/// should work + */ +export type CommentWithSlashes = number; + +/** + * Testing expression placeholders in string: ${expression} should work + */ +export type CommentWithExpressionPlaceholders = number; + +/** + * Testing quotes in string: 'single quote''' and "double quotes""" should work + */ +export type CommentWithQuotes = number; + +/** + * Testing reserved characters in string: * inline * and ** inline ** should work + */ +export type CommentWithReservedCharacters = number; + +/** + * This is a simple number + */ +export type SimpleInteger = number; + +/** + * This is a simple boolean + */ +export type SimpleBoolean = boolean; + +/** + * This is a simple string + */ +export type SimpleString = string; + +/** + * A string with non-ascii (unicode) characters valid in typescript identifiers (æøåÆØÅöÔèÈ字符串) + */ +export type NonAsciiStringæøåÆØÅöôêÊ字符串 = string; + +/** + * This is a simple file + */ +export type SimpleFile = (Blob | File); + +/** + * This is a simple reference + */ +export type SimpleReference = ModelWithString; + +/** + * This is a simple string + */ +export type SimpleStringWithPattern = string | null; + +/** + * This is a simple enum with strings + */ +export type EnumWithStrings = 'Success' | 'Warning' | 'Error' | "'Single Quote'" | '"Double Quotes"' | 'Non-ascii: øæåôöØÆÅÔÖ字符串'; + +/** + * This is a simple enum with strings + */ +export const EnumWithStrings = { + SUCCESS: 'Success', + WARNING: 'Warning', + ERROR: 'Error', + _SINGLE_QUOTE_: "'Single Quote'", + _DOUBLE_QUOTES_: '"Double Quotes"', + 'NON_ASCII__ØÆÅÔÖ_ØÆÅÔÖ字符串': 'Non-ascii: øæåôöØÆÅÔÖ字符串' +} as const; + +export type EnumWithReplacedCharacters = "'Single Quote'" | '"Double Quotes"' | 'øæåôöØÆÅÔÖ字符串' | 3.1 | ''; + +export const EnumWithReplacedCharacters = { + _SINGLE_QUOTE_: "'Single Quote'", + _DOUBLE_QUOTES_: '"Double Quotes"', + 'ØÆÅÔÖ_ØÆÅÔÖ字符串': 'øæåôöØÆÅÔÖ字符串', + '_3.1': 3.1, + EMPTY_STRING: '' +} as const; + +/** + * This is a simple enum with numbers + */ +export type EnumWithNumbers = 1 | 2 | 3 | 1.1 | 1.2 | 1.3 | 100 | 200 | 300 | -100 | -200 | -300 | -1.1 | -1.2 | -1.3; + +/** + * This is a simple enum with numbers + */ +export const EnumWithNumbers = { + '_1': 1, + '_2': 2, + '_3': 3, + '_1.1': 1.1, + '_1.2': 1.2, + '_1.3': 1.3, + '_100': 100, + '_200': 200, + '_300': 300, + '_-100': -100, + '_-200': -200, + '_-300': -300, + '_-1.1': -1.1, + '_-1.2': -1.2, + '_-1.3': -1.3 +} as const; + +/** + * Success=1,Warning=2,Error=3 + */ +export type EnumFromDescription = number; + +/** + * This is a simple enum with numbers + */ +export type EnumWithExtensions = 200 | 400 | 500; + +/** + * This is a simple enum with numbers + */ +export const EnumWithExtensions = { + /** + * Used when the status of something is successful + */ + CUSTOM_SUCCESS: 200, + /** + * Used when the status of something has a warning + */ + CUSTOM_WARNING: 400, + /** + * Used when the status of something has an error + */ + CUSTOM_ERROR: 500 +} as const; + +export type EnumWithXEnumNames = 0 | 1 | 2; + +export const EnumWithXEnumNames = { + zero: 0, + one: 1, + two: 2 +} as const; + +/** + * This is a simple array with numbers + */ +export type ArrayWithNumbers = Array<(number)>; + +/** + * This is a simple array with booleans + */ +export type ArrayWithBooleans = Array<(boolean)>; + +/** + * This is a simple array with strings + */ +export type ArrayWithStrings = Array<(string)>; + +/** + * This is a simple array with references + */ +export type ArrayWithReferences = Array; + +/** + * This is a simple array containing an array + */ +export type ArrayWithArray = Array>; + +/** + * This is a simple array with properties + */ +export type ArrayWithProperties = Array<{ + '16x16'?: camelCaseCommentWithBreaks; + bar?: string; +}>; + +/** + * This is a simple array with any of properties + */ +export type ArrayWithAnyOfProperties = Array<({ + foo?: string; +} | { + bar?: string; +})>; + +export type AnyOfAnyAndNull = { + data?: unknown | null; +}; + +/** + * This is a simple array with any of properties + */ +export type AnyOfArrays = { + results?: Array<({ + foo?: string; +} | { + bar?: string; +})>; +}; + +/** + * This is a string dictionary + */ +export type DictionaryWithString = { + [key: string]: (string); +}; + +export type DictionaryWithPropertiesAndAdditionalProperties = { + foo?: number; + bar?: boolean; + '[key: string]': (string | number | boolean) | undefined; +}; + +/** + * This is a string reference + */ +export type DictionaryWithReference = { + [key: string]: ModelWithString; +}; + +/** + * This is a complex dictionary + */ +export type DictionaryWithArray = { + [key: string]: Array; +}; + +/** + * This is a string dictionary + */ +export type DictionaryWithDictionary = { + [key: string]: { + [key: string]: (string); + }; +}; + +/** + * This is a complex dictionary + */ +export type DictionaryWithProperties = { + [key: string]: { + foo?: string; + bar?: string; + }; +}; + +/** + * This is a model with one number property + */ +export type ModelWithInteger = { + /** + * This is a simple number property + */ + prop?: number; +}; + +/** + * This is a model with one boolean property + */ +export type ModelWithBoolean = { + /** + * This is a simple boolean property + */ + prop?: boolean; +}; + +/** + * This is a model with one string property + */ +export type ModelWithString = { + /** + * This is a simple string property + */ + prop?: string; +}; + +/** + * This is a model with one string property + */ +export type ModelWithStringError = { + /** + * This is a simple string property + */ + prop?: string; +}; + +/** + * `Comment` or `VoiceComment`. The JSON object for adding voice comments to tickets is different. See [Adding voice comments to tickets](/documentation/ticketing/managing-tickets/adding-voice-comments-to-tickets) + */ +export type Model_From_Zendesk = string; + +/** + * This is a model with one string property + */ +export type ModelWithNullableString = { + /** + * This is a simple string property + */ + nullableProp1?: string | null; + /** + * This is a simple string property + */ + nullableRequiredProp1: string | null; + /** + * This is a simple string property + */ + nullableProp2?: string | null; + /** + * This is a simple string property + */ + nullableRequiredProp2: string | null; + /** + * This is a simple enum with strings + */ + 'foo_bar-enum'?: 'Success' | 'Warning' | 'Error' | 'ØÆÅ字符串'; +}; + +/** + * This is a simple enum with strings + */ +export type foo_bar_enum = 'Success' | 'Warning' | 'Error' | 'ØÆÅ字符串'; + +/** + * This is a simple enum with strings + */ +export const foo_bar_enum = { + SUCCESS: 'Success', + WARNING: 'Warning', + ERROR: 'Error', + 'ØÆÅ字符串': 'ØÆÅ字符串' +} as const; + +/** + * This is a model with one enum + */ +export type ModelWithEnum = { + /** + * This is a simple enum with strings + */ + 'foo_bar-enum'?: 'Success' | 'Warning' | 'Error' | 'ØÆÅ字符串'; + /** + * These are the HTTP error code enums + */ + statusCode?: '100' | '200 FOO' | '300 FOO_BAR' | '400 foo-bar' | '500 foo.bar' | '600 foo&bar'; + /** + * Simple boolean enum + */ + bool?: boolean; +}; + +/** + * These are the HTTP error code enums + */ +export type statusCode = '100' | '200 FOO' | '300 FOO_BAR' | '400 foo-bar' | '500 foo.bar' | '600 foo&bar'; + +/** + * These are the HTTP error code enums + */ +export const statusCode = { + _100: '100', + _200_FOO: '200 FOO', + _300_FOO_BAR: '300 FOO_BAR', + _400_FOO_BAR: '400 foo-bar', + _500_FOO_BAR: '500 foo.bar', + _600_FOO_BAR: '600 foo&bar' +} as const; + +/** + * This is a model with one enum with escaped name + */ +export type ModelWithEnumWithHyphen = { + 'foo-bar-baz-qux'?: '3.0'; +}; + +export type foo_bar_baz_qux = '3.0'; + +export const foo_bar_baz_qux = { + _3_0: '3.0' +} as const; + +/** + * This is a model with one enum + */ +export type ModelWithEnumFromDescription = { + /** + * Success=1,Warning=2,Error=3 + */ + test?: number; +}; + +/** + * This is a model with nested enums + */ +export type ModelWithNestedEnums = { + dictionaryWithEnum?: { + [key: string]: ('Success' | 'Warning' | 'Error'); + }; + dictionaryWithEnumFromDescription?: { + [key: string]: (number); + }; + arrayWithEnum?: Array<('Success' | 'Warning' | 'Error')>; + arrayWithDescription?: Array<(number)>; + /** + * This is a simple enum with strings + */ + 'foo_bar-enum'?: 'Success' | 'Warning' | 'Error' | 'ØÆÅ字符串'; +}; + +/** + * This is a model with one property containing a reference + */ +export type ModelWithReference = { + prop?: ModelWithProperties; +}; + +/** + * This is a model with one property containing an array + */ +export type ModelWithArrayReadOnlyAndWriteOnly = { + prop?: Array; + propWithFile?: Array<((Blob | File))>; + propWithNumber?: Array<(number)>; +}; + +/** + * This is a model with one property containing an array + */ +export type ModelWithArray = { + prop?: Array; + propWithFile?: Array<((Blob | File))>; + propWithNumber?: Array<(number)>; +}; + +/** + * This is a model with one property containing a dictionary + */ +export type ModelWithDictionary = { + prop?: { + [key: string]: (string); + }; +}; + +/** + * This is a deprecated model with a deprecated property + * @deprecated + */ +export type DeprecatedModel = { + /** + * This is a deprecated property + * @deprecated + */ + prop?: string; +}; + +/** + * This is a model with one property containing a circular reference + */ +export type ModelWithCircularReference = { + prop?: ModelWithCircularReference; +}; + +/** + * This is a model with one property with a 'one of' relationship + */ +export type CompositionWithOneOf = { + propA?: ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary; +}; + +/** + * This is a model with one property with a 'one of' relationship where the options are not $ref + */ +export type CompositionWithOneOfAnonymous = { + propA?: { + propA?: string; +} | string | number; +}; + +/** + * Circle + */ +export type ModelCircle = { + kind: 'circle'; + radius?: number; +}; + +/** + * Square + */ +export type ModelSquare = { + kind: 'square'; + sideLength?: number; +}; + +/** + * This is a model with one property with a 'one of' relationship where the options are not $ref + */ +export type CompositionWithOneOfDiscriminator = ModelCircle | ModelSquare; + +/** + * This is a model with one property with a 'any of' relationship + */ +export type CompositionWithAnyOf = { + propA?: ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary; +}; + +/** + * This is a model with one property with a 'any of' relationship where the options are not $ref + */ +export type CompositionWithAnyOfAnonymous = { + propA?: { + propA?: string; +} | string | number; +}; + +/** + * This is a model with nested 'any of' property with a type null + */ +export type CompositionWithNestedAnyAndTypeNull = { + propA?: Array<(ModelWithDictionary | null)> | Array<(ModelWithArray | null)>; +}; + +export type e_num_1Период = 'Bird' | 'Dog'; + +export const e_num_1Период = { + BIRD: 'Bird', + DOG: 'Dog' +} as const; + +export type ConstValue = "ConstValue"; + +/** + * This is a model with one property with a 'any of' relationship where the options are not $ref + */ +export type CompositionWithNestedAnyOfAndNull = { + propA?: Array<(e_num_1Период | ConstValue)> | null; +}; + +/** + * This is a model with one property with a 'one of' relationship + */ +export type CompositionWithOneOfAndNullable = { + propA?: { + boolean?: boolean; +} | ModelWithEnum | ModelWithArray | ModelWithDictionary | null; +}; + +/** + * This is a model that contains a simple dictionary within composition + */ +export type CompositionWithOneOfAndSimpleDictionary = { + propA?: boolean | { + [key: string]: (number); +}; +}; + +/** + * This is a model that contains a dictionary of simple arrays within composition + */ +export type CompositionWithOneOfAndSimpleArrayDictionary = { + propA?: boolean | { + [key: string]: Array<(boolean)>; +}; +}; + +/** + * This is a model that contains a dictionary of complex arrays (composited) within composition + */ +export type CompositionWithOneOfAndComplexArrayDictionary = { + propA?: boolean | { + [key: string]: Array<(number | string)>; +}; +}; + +/** + * This is a model with one property with a 'all of' relationship + */ +export type CompositionWithAllOfAndNullable = { + propA?: ({ + boolean?: boolean; +} & ModelWithEnum & ModelWithArray & ModelWithDictionary) | null; +}; + +/** + * This is a model with one property with a 'any of' relationship + */ +export type CompositionWithAnyOfAndNullable = { + propA?: { + boolean?: boolean; +} | ModelWithEnum | ModelWithArray | ModelWithDictionary | null; +}; + +/** + * This is a base model with two simple optional properties + */ +export type CompositionBaseModel = { + firstName?: string; + lastname?: string; +}; + +/** + * This is a model that extends the base model + */ +export type CompositionExtendedModel = CompositionBaseModel & { + firstName: string; + lastname: string; + age: number; +}; + +/** + * This is a model with one nested property + */ +export type ModelWithProperties = { + required: string; + readonly requiredAndReadOnly: string; + requiredAndNullable: string | null; + string?: string; + number?: number; + boolean?: boolean; + reference?: ModelWithString; + 'property with space'?: string; + default?: string; + try?: string; + readonly '@namespace.string'?: string; + readonly '@namespace.integer'?: number; +}; + +/** + * This is a model with one nested property + */ +export type ModelWithNestedProperties = { + readonly first: { + readonly second: { + readonly third: string | null; + } | null; + } | null; +}; + +/** + * This is a model with duplicated properties + */ +export type ModelWithDuplicateProperties = { + prop?: ModelWithString; +}; + +/** + * This is a model with ordered properties + */ +export type ModelWithOrderedProperties = { + zebra?: string; + apple?: string; + hawaii?: string; +}; + +/** + * This is a model with duplicated imports + */ +export type ModelWithDuplicateImports = { + propA?: ModelWithString; + propB?: ModelWithString; + propC?: ModelWithString; +}; + +/** + * This is a model that extends another model + */ +export type ModelThatExtends = ModelWithString & { + propExtendsA?: string; + propExtendsB?: ModelWithString; +}; + +/** + * This is a model that extends another model + */ +export type ModelThatExtendsExtends = ModelWithString & ModelThatExtends & { + propExtendsC?: string; + propExtendsD?: ModelWithString; +}; + +/** + * This is a model that contains a some patterns + */ +export type ModelWithPattern = { + key: string; + name: string; + readonly enabled?: boolean; + readonly modified?: string; + id?: string; + text?: string; + patternWithSingleQuotes?: string; + patternWithNewline?: string; + patternWithBacktick?: string; +}; + +export type File = { + readonly id?: string; + readonly updated_at?: string; + readonly created_at?: string; + mime: string; + readonly file?: string; +}; + +export type _default = { + name?: string; +}; + +export type Pageable = { + page?: number; + size?: number; + sort?: Array<(string)>; +}; + +/** + * This is a free-form object without additionalProperties. + */ +export type FreeFormObjectWithoutAdditionalProperties = { + [key: string]: unknown; +}; + +/** + * This is a free-form object with additionalProperties: true. + */ +export type FreeFormObjectWithAdditionalPropertiesEqTrue = { + [key: string]: unknown; +}; + +/** + * This is a free-form object with additionalProperties: {}. + */ +export type FreeFormObjectWithAdditionalPropertiesEqEmptyObject = { + [key: string]: unknown; +}; + +export type ModelWithConst = { + String?: "String"; + number?: 0; + null?: null; + withType?: "Some string"; +}; + +/** + * This is a model with one property and additionalProperties: true + */ +export type ModelWithAdditionalPropertiesEqTrue = { + /** + * This is a simple string property + */ + prop?: string; + '[key: string]': unknown | string; +}; + +export type NestedAnyOfArraysNullable = { + nullableArray?: Array<(string | boolean)> | null; +}; + +export type CompositionWithOneOfAndProperties = { + foo: ParameterSimpleParameter; +} | { + bar: NonAsciiStringæøåÆØÅöôêÊ字符串; +} & { + baz: number | null; + qux: number; +}; + +/** + * An object that can be null + */ +export type NullableObject = { + foo?: string; +} | null; + +/** + * Some % character + */ +export type CharactersInDescription = string; + +export type ModelWithNullableObject = { + data?: NullableObject; +}; + +export type ModelWithOneOfEnum = { + foo: 'Bar'; +} | { + foo: 'Baz'; +} | { + foo: 'Qux'; +} | { + content: string; + foo: 'Quux'; +} | { + content: [ + string, + string + ]; + foo: 'Corge'; +}; + +export type foo = 'Bar'; + +export const foo = { + BAR: 'Bar' +} as const; + +export type ModelWithNestedArrayEnumsDataFoo = 'foo' | 'bar'; + +export const ModelWithNestedArrayEnumsDataFoo = { + FOO: 'foo', + BAR: 'bar' +} as const; + +export type ModelWithNestedArrayEnumsDataBar = 'baz' | 'qux'; + +export const ModelWithNestedArrayEnumsDataBar = { + BAZ: 'baz', + QUX: 'qux' +} as const; + +export type ModelWithNestedArrayEnumsData = { + foo?: Array; + bar?: Array; +}; + +export type ModelWithNestedArrayEnums = { + array_strings?: Array<(string)>; + data?: ModelWithNestedArrayEnumsData; +}; + +export type ModelWithNestedCompositionEnums = { + foo?: ModelWithNestedArrayEnumsDataFoo; +}; + +export type ModelWithReadOnlyAndWriteOnly = { + foo: string; + readonly bar: string; + baz: string; +}; + +export type ModelWithConstantSizeArray = [ + number, + number +]; + +export type ModelWithAnyOfConstantSizeArray = [ + number | string, + number | string, + number | string +]; + +export type ModelWithPrefixItemsConstantSizeArray = [ + ModelWithInteger, + number | string, + string +]; + +export type ModelWithAnyOfConstantSizeArrayNullable = [ + number | null | string, + number | null | string, + number | null | string +]; + +export type ModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = [ + number | _import, + number | _import +]; + +export type ModelWithAnyOfConstantSizeArrayAndIntersect = [ + number & string, + number & string +]; + +export type ModelWithNumericEnumUnion = { + /** + * Период + */ + value?: -10 | -1 | 0 | 1 | 3 | 6 | 12; +}; + +/** + * Период + */ +export type value = -10 | -1 | 0 | 1 | 3 | 6 | 12; + +/** + * Период + */ +export const value = { + '_-10': -10, + '_-1': -1, + '_0': 0, + '_1': 1, + '_3': 3, + '_6': 6, + '_12': 12 +} as const; + +/** + * Some description with `back ticks` + */ +export type ModelWithBackticksInDescription = { + /** + * The template `that` should be used for parsing and importing the contents of the CSV file. + * + *

There is one placeholder currently supported:

  • ${x} - refers to the n-th column in the CSV file, e.g. ${1}, ${2}, ...)

Example of a correct JSON template:

+ *
+     * [
+     * {
+     * "resourceType": "Asset",
+     * "identifier": {
+     * "name": "${1}",
+     * "domain": {
+     * "name": "${2}",
+     * "community": {
+     * "name": "Some Community"
+     * }
+     * }
+     * },
+     * "attributes" : {
+     * "00000000-0000-0000-0000-000000003115" : [ {
+     * "value" : "${3}"
+     * } ],
+     * "00000000-0000-0000-0000-000000000222" : [ {
+     * "value" : "${4}"
+     * } ]
+     * }
+     * }
+     * ]
+     * 
+ */ + template?: string; +}; + +export type ModelWithOneOfAndProperties = ParameterSimpleParameter | NonAsciiStringæøåÆØÅöôêÊ字符串 & { + baz: number | null; + qux: number; +}; + +/** + * Model used to test deduplication strategy (unused) + */ +export type ParameterSimpleParameterUnused = string; + +/** + * Model used to test deduplication strategy + */ +export type PostServiceWithEmptyTagResponse = string; + +/** + * Model used to test deduplication strategy + */ +export type PostServiceWithEmptyTagResponse2 = string; + +/** + * Model used to test deduplication strategy + */ +export type DeleteFooData = string; + +/** + * Model used to test deduplication strategy + */ +export type DeleteFooData2 = string; + +/** + * Model with restricted keyword name + */ +export type _import = string; + +/** + * This is a reusable parameter + */ +export type ParameterSimpleParameter = string; + +/** + * Parameter with illegal characters + */ +export type Parameterx_Foo_Bar = ModelWithString; + +export type ImportData = { + body: ModelWithReadOnlyAndWriteOnly | ModelWithArrayReadOnlyAndWriteOnly; +}; + +export type ImportResponse = Model_From_Zendesk | ModelWithReadOnlyAndWriteOnly; + +export type ImportError = unknown; + +export type ApiVversionOdataControllerCountResponse = Model_From_Zendesk; + +export type ApiVversionOdataControllerCountError = unknown; + +export type DeleteFooData3 = { + headers: { + /** + * Parameter with illegal characters + */ + 'x-Foo-Bar': ModelWithString; + }; + path: { + /** + * bar in method + */ + BarParam: string; + /** + * foo in method + */ + foo_param: string; + }; +}; + +export type CallWithDescriptionsData = { + query?: { + /** + * Testing backticks in string: `backticks` and ```multiple backticks``` should work + */ + parameterWithBackticks?: unknown; + /** + * Testing multiline comments in string: First line + * Second line + * + * Fourth line + */ + parameterWithBreaks?: unknown; + /** + * Testing expression placeholders in string: ${expression} should work + */ + parameterWithExpressionPlaceholders?: unknown; + /** + * Testing quotes in string: 'single quote''' and "double quotes""" should work + */ + parameterWithQuotes?: unknown; + /** + * Testing reserved characters in string: * inline * and ** inline ** should work + */ + parameterWithReservedCharacters?: unknown; + /** + * Testing slashes in string: \backwards\\\ and /forwards/// should work + */ + parameterWithSlashes?: unknown; + }; +}; + +export type DeprecatedCallData = { + headers: { + /** + * This parameter is deprecated + * @deprecated + */ + parameter: DeprecatedModel | null; + }; +}; + +export type CallWithParametersData = { + /** + * This is the parameter that goes into the body + */ + body: ModelWithString | null; + headers: { + /** + * This is the parameter that goes into the header + */ + parameterHeader: string | null; + }; + path: { + /** + * This is the parameter that goes into the path + */ + parameterPath: string | null; + }; + query: { + foo_all_of_enum: ModelWithNestedArrayEnumsDataFoo; + foo_ref_enum?: ModelWithNestedArrayEnumsDataFoo; + /** + * This is the parameter that goes into the query params + */ + parameterQuery: string | null; + }; +}; + +export type CallWithWeirdParameterNamesData = { + /** + * This is the parameter that goes into the body + */ + body: ModelWithString | null; + headers: { + /** + * This is the parameter that goes into the request header + */ + 'parameter.header': string | null; + }; + path?: { + /** + * This is the parameter that goes into the path + */ + 'parameter-path-2'?: string; + /** + * This is the parameter that goes into the path + */ + 'PARAMETER-PATH-3'?: string; + /** + * This is the parameter that goes into the path + */ + 'parameter.path.1'?: string; + }; + query: { + /** + * This is the parameter with a reserved keyword + */ + default?: string; + /** + * This is the parameter that goes into the request query params + */ + 'parameter-query': string | null; + }; +}; + +export type GetCallWithOptionalParamData = { + /** + * This is a required parameter + */ + body: ModelWithOneOfEnum; + query?: { + /** + * This is an optional parameter + */ + parameter?: string; + }; +}; + +export type PostCallWithOptionalParamData = { + /** + * This is an optional parameter + */ + body?: ModelWithString; + query: { + /** + * This is a required parameter + */ + parameter: Pageable; + }; +}; + +export type PostApiRequestBodyData = { + /** + * A reusable request body + */ + body?: ModelWithString; + query?: { + /** + * This is a reusable parameter + */ + parameter?: string; + }; +}; + +export type PostApiFormDataData = { + /** + * A reusable request body + */ + body?: ModelWithString; + query?: { + /** + * This is a reusable parameter + */ + parameter?: string; + }; +}; + +export type CallWithDefaultParametersData = { + query?: { + /** + * This is a simple boolean with default value + */ + parameterBoolean?: boolean | null; + /** + * This is a simple enum with default value + */ + parameterEnum?: 'Success' | 'Warning' | 'Error'; + /** + * This is a simple model with default value + */ + parameterModel?: ModelWithString | null; + /** + * This is a simple number with default value + */ + parameterNumber?: number | null; + /** + * This is a simple string with default value + */ + parameterString?: string | null; + }; +}; + +export type CallWithDefaultOptionalParametersData = { + query?: { + /** + * This is a simple boolean that is optional with default value + */ + parameterBoolean?: boolean; + /** + * This is a simple enum that is optional with default value + */ + parameterEnum?: 'Success' | 'Warning' | 'Error'; + /** + * This is a simple model that is optional with default value + */ + parameterModel?: ModelWithString; + /** + * This is a simple number that is optional with default value + */ + parameterNumber?: number; + /** + * This is a simple string that is optional with default value + */ + parameterString?: string; + }; +}; + +export type CallToTestOrderOfParamsData = { + query: { + /** + * This is a optional string with default + */ + parameterOptionalStringWithDefault?: string; + /** + * This is a optional string with empty default + */ + parameterOptionalStringWithEmptyDefault?: string; + /** + * This is a optional string with no default + */ + parameterOptionalStringWithNoDefault?: string; + /** + * This is a string that can be null with default + */ + parameterStringNullableWithDefault?: string | null; + /** + * This is a string that can be null with no default + */ + parameterStringNullableWithNoDefault?: string | null; + /** + * This is a string with default + */ + parameterStringWithDefault: string; + /** + * This is a string with empty default + */ + parameterStringWithEmptyDefault: string; + /** + * This is a string with no default + */ + parameterStringWithNoDefault: string; + }; +}; + +export type CallWithNoContentResponseResponse = void; + +export type CallWithNoContentResponseError = unknown; + +export type CallWithResponseAndNoContentResponseResponse = number | void; + +export type CallWithResponseAndNoContentResponseError = unknown; + +export type DummyAResponse = void; + +export type DummyAError = unknown; + +export type DummyBResponse = void; + +export type DummyBError = unknown; + +export type CallWithResponseResponse = _import; + +export type CallWithResponseError = unknown; + +export type CallWithDuplicateResponsesResponse = ModelWithBoolean & ModelWithInteger | ModelWithString; + +export type CallWithDuplicateResponsesError = ModelWithStringError & DictionaryWithArray & ModelWithBoolean; + +export type CallWithResponsesResponse = { + readonly '@namespace.string'?: string; + readonly '@namespace.integer'?: number; + readonly value?: Array; +} | ModelThatExtends | ModelThatExtendsExtends; + +export type CallWithResponsesError = ModelWithStringError; + +export type CollectionFormatData = { + query: { + /** + * This is an array parameter that is sent as csv format (comma-separated values) + */ + parameterArrayCSV: Array<(string)> | null; + /** + * This is an array parameter that is sent as multi format (multiple parameter instances) + */ + parameterArrayMulti: Array<(string)> | null; + /** + * This is an array parameter that is sent as pipes format (pipe-separated values) + */ + parameterArrayPipes: Array<(string)> | null; + /** + * This is an array parameter that is sent as ssv format (space-separated values) + */ + parameterArraySSV: Array<(string)> | null; + /** + * This is an array parameter that is sent as tsv format (tab-separated values) + */ + parameterArrayTSV: Array<(string)> | null; + }; +}; + +export type TypesData = { + path?: { + /** + * This is a number parameter + */ + id?: number; + }; + query: { + /** + * This is an array parameter + */ + parameterArray: Array<(string)> | null; + /** + * This is a boolean parameter + */ + parameterBoolean: boolean | null; + /** + * This is a dictionary parameter + */ + parameterDictionary: { + [key: string]: unknown; + } | null; + /** + * This is an enum parameter + */ + parameterEnum: 'Success' | 'Warning' | 'Error' | null; + /** + * This is a number parameter + */ + parameterNumber: number; + /** + * This is an object parameter + */ + parameterObject: { + [key: string]: unknown; + } | null; + /** + * This is a string parameter + */ + parameterString: string | null; + }; +}; + +export type TypesResponse = number | string | boolean | { + [key: string]: unknown; +}; + +export type TypesError = unknown; + +export type UploadFileData = unknown; + +export type UploadFileResponse = boolean; + +export type UploadFileError = unknown; + +export type FileResponseData = { + path: { + id: string; + }; +}; + +export type FileResponseResponse = (Blob | File); + +export type FileResponseError = unknown; + +export type ComplexTypesData = { + query: { + /** + * Parameter containing object + */ + parameterObject: { + first?: { + second?: { + third?: string; + }; + }; + }; + /** + * Parameter containing reference + */ + parameterReference: ModelWithString; + }; +}; + +export type ComplexTypesResponse = Array; + +export type ComplexTypesError = unknown; + +export type MultipartRequestData = { + body?: { + content?: (Blob | File); + data?: ModelWithString | null; + }; +}; + +export type MultipartResponseResponse = { + file?: (Blob | File); + metadata?: { + foo?: string; + bar?: string; + }; +}; + +export type MultipartResponseError = unknown; + +export type ComplexParamsData = { + body?: { + readonly key: string | null; + name: string | null; + enabled?: boolean; + readonly type: 'Monkey' | 'Horse' | 'Bird'; + listOfModels?: Array | null; + listOfStrings?: Array<(string)> | null; + parameters: ModelWithString | ModelWithEnum | ModelWithArray | ModelWithDictionary; + readonly user?: { + readonly id?: number; + readonly name?: string | null; + }; + }; + path: { + id: number; + }; +}; + +export type ComplexParamsResponse = ModelWithString; + +export type ComplexParamsError = unknown; + +export type CallWithResultFromHeaderResponse = string; + +export type CallWithResultFromHeaderError = unknown; + +export type TestErrorCodeData = { + query: { + /** + * Status code to return + */ + status: number; + }; +}; + +export type TestErrorCodeResponse = unknown; + +export type TestErrorCodeError = unknown; + +export type NonAsciiæøåÆøÅöôêÊ字符串Data = { + query: { + /** + * Dummy input param + */ + nonAsciiParamæøåÆØÅöôêÊ: number; + }; +}; + +export type NonAsciiæøåÆøÅöôêÊ字符串Response = Array; + +export type NonAsciiæøåÆøÅöôêÊ字符串Error = unknown; + +export type PutWithFormUrlEncodedData = { + body: ArrayWithStrings; +}; + +export type $OpenApiTs = { + '/api/v{api-version}/no-tag': { + post: { + req: ImportData; + res: { + /** + * Success + */ + '200': Model_From_Zendesk; + /** + * Default success response + */ + default: ModelWithReadOnlyAndWriteOnly; + }; + }; + }; + '/api/v{api-version}/simple/$count': { + get: { + res: { + /** + * Success + */ + '200': Model_From_Zendesk; + }; + }; + }; + '/api/v{api-version}/foo/{foo_param}/bar/{BarParam}': { + delete: { + req: DeleteFooData3; + }; + }; + '/api/v{api-version}/descriptions/': { + post: { + req: CallWithDescriptionsData; + }; + }; + '/api/v{api-version}/parameters/deprecated': { + post: { + req: DeprecatedCallData; + }; + }; + '/api/v{api-version}/parameters/{parameterPath}': { + post: { + req: CallWithParametersData; + }; + }; + '/api/v{api-version}/parameters/{parameter.path.1}/{parameter-path-2}/{PARAMETER-PATH-3}': { + post: { + req: CallWithWeirdParameterNamesData; + }; + }; + '/api/v{api-version}/parameters/': { + get: { + req: GetCallWithOptionalParamData; + }; + post: { + req: PostCallWithOptionalParamData; + }; + }; + '/api/v{api-version}/requestBody/': { + post: { + req: PostApiRequestBodyData; + }; + }; + '/api/v{api-version}/formData/': { + post: { + req: PostApiFormDataData; + }; + }; + '/api/v{api-version}/defaults': { + get: { + req: CallWithDefaultParametersData; + }; + post: { + req: CallWithDefaultOptionalParametersData; + }; + put: { + req: CallToTestOrderOfParamsData; + }; + }; + '/api/v{api-version}/no-content': { + get: { + res: { + /** + * Success + */ + '204': void; + }; + }; + }; + '/api/v{api-version}/multiple-tags/response-and-no-content': { + get: { + res: { + /** + * Response is a simple number + */ + '200': number; + /** + * Success + */ + '204': void; + }; + }; + }; + '/api/v{api-version}/multiple-tags/a': { + get: { + res: { + /** + * Success + */ + '204': void; + }; + }; + }; + '/api/v{api-version}/multiple-tags/b': { + get: { + res: { + /** + * Success + */ + '204': void; + }; + }; + }; + '/api/v{api-version}/response': { + get: { + res: { + default: _import; + }; + }; + post: { + res: { + /** + * Message for 200 response + */ + '200': ModelWithBoolean & ModelWithInteger; + /** + * Message for 201 response + */ + '201': ModelWithString; + /** + * Message for 202 response + */ + '202': ModelWithString; + /** + * Message for 500 error + */ + '500': ModelWithStringError; + /** + * Message for 501 error + */ + '501': ModelWithStringError; + /** + * Message for 502 error + */ + '502': ModelWithStringError; + /** + * Message for 4XX errors + */ + '4XX': DictionaryWithArray; + /** + * Default error response + */ + default: ModelWithBoolean; + }; + }; + put: { + res: { + /** + * Message for 200 response + */ + '200': { + readonly '@namespace.string'?: string; + readonly '@namespace.integer'?: number; + readonly value?: Array; + }; + /** + * Message for 201 response + */ + '201': ModelThatExtends; + /** + * Message for 202 response + */ + '202': ModelThatExtendsExtends; + /** + * Message for 500 error + */ + '500': ModelWithStringError; + /** + * Message for 501 error + */ + '501': ModelWithStringError; + /** + * Message for 502 error + */ + '502': ModelWithStringError; + /** + * Message for default response + */ + default: ModelWithStringError; + }; + }; + }; + '/api/v{api-version}/collectionFormat': { + get: { + req: CollectionFormatData; + }; + }; + '/api/v{api-version}/types': { + get: { + req: TypesData; + res: { + /** + * Response is a simple number + */ + '200': number; + /** + * Response is a simple string + */ + '201': string; + /** + * Response is a simple boolean + */ + '202': boolean; + /** + * Response is a simple object + */ + '203': { + [key: string]: unknown; + }; + }; + }; + }; + '/api/v{api-version}/upload': { + post: { + req: UploadFileData; + res: { + '200': boolean; + }; + }; + }; + '/api/v{api-version}/file/{id}': { + get: { + req: FileResponseData; + res: { + /** + * Success + */ + '200': (Blob | File); + }; + }; + }; + '/api/v{api-version}/complex': { + get: { + req: ComplexTypesData; + res: { + /** + * Successful response + */ + '200': Array; + /** + * 400 `server` error + */ + '400': unknown; + /** + * 500 server error + */ + '500': unknown; + }; + }; + }; + '/api/v{api-version}/multipart': { + post: { + req: MultipartRequestData; + }; + get: { + res: { + /** + * OK + */ + '200': { + file?: (Blob | File); + metadata?: { + foo?: string; + bar?: string; + }; + }; + }; + }; + }; + '/api/v{api-version}/complex/{id}': { + put: { + req: ComplexParamsData; + res: { + /** + * Success + */ + '200': ModelWithString; + }; + }; + }; + '/api/v{api-version}/header': { + post: { + res: { + /** + * Successful response + */ + '200': string; + /** + * 400 server error + */ + '400': unknown; + /** + * 500 server error + */ + '500': unknown; + }; + }; + }; + '/api/v{api-version}/error': { + post: { + req: TestErrorCodeData; + res: { + /** + * Custom message: Successful response + */ + '200': unknown; + /** + * Custom message: Internal Server Error + */ + '500': unknown; + /** + * Custom message: Not Implemented + */ + '501': unknown; + /** + * Custom message: Bad Gateway + */ + '502': unknown; + /** + * Custom message: Service Unavailable + */ + '503': unknown; + }; + }; + }; + '/api/v{api-version}/non-ascii-æøåÆØÅöôêÊ字符串': { + post: { + req: NonAsciiæøåÆøÅöôêÊ字符串Data; + res: { + /** + * Successful response + */ + '200': Array; + }; + }; + put: { + req: PutWithFormUrlEncodedData; + }; + }; +}; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/client.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/client.ts.snap new file mode 100644 index 000000000..ddf1b548b --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/client.ts.snap @@ -0,0 +1,7 @@ +export { client, createClient } from './core/'; +export type { Client, Options, RequestResult } from './core/types'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from './core/utils'; diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/index.ts.snap new file mode 100644 index 000000000..4db32ef16 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/index.ts.snap @@ -0,0 +1,163 @@ +import type { Client, Config, RequestOptions } from './types'; +import { + createDefaultConfig, + createInterceptors, + createQuerySerializer, + getParseAs, + getUrl, + mergeHeaders, +} from './utils'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +let globalConfig = createDefaultConfig(); + +const globalInterceptors = createInterceptors< + Request, + Response, + RequestOptions +>(); + +export const createClient = (config: Config): Client => { + const defaultConfig = createDefaultConfig(); + const _config = { ...defaultConfig, ...config }; + + if (_config.baseUrl?.endsWith('/')) { + _config.baseUrl = _config.baseUrl.substring(0, _config.baseUrl.length - 1); + } + _config.headers = mergeHeaders(defaultConfig.headers, _config.headers); + + if (_config.global) { + globalConfig = { ..._config }; + } + + // @ts-ignore + const getConfig = () => (_config.root ? globalConfig : _config); + + const interceptors = _config.global + ? globalInterceptors + : createInterceptors(); + + // @ts-ignore + const request: Client['request'] = async (options) => { + const config = getConfig(); + + const opts: RequestOptions = { + ...config, + ...options, + headers: mergeHeaders(config.headers, options.headers), + }; + if (opts.body && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body); + } + + const url = getUrl({ + baseUrl: opts.baseUrl ?? '', + path: opts.path, + query: opts.query, + querySerializer: + typeof opts.querySerializer === 'function' + ? opts.querySerializer + : createQuerySerializer(opts.querySerializer), + url: opts.url, + }); + + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request._fns) { + request = await fn(request, opts); + } + + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response._fns) { + response = await fn(response, request, opts); + } + + const result = { + request, + response, + }; + + // return empty objects so truthy checks for data/error succeed + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + if (response.ok) { + return { + data: {}, + ...result, + }; + } + return { + error: {}, + ...result, + }; + } + + if (response.ok) { + if (opts.parseAs === 'stream') { + return { + data: response.body, + ...result, + }; + } + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + let data = await response[parseAs](); + if (parseAs === 'json' && options.responseTransformer) { + data = await options.responseTransformer(data); + } + + return { + data, + ...result, + }; + } + + let error = await response.text(); + try { + error = JSON.parse(error); + } catch { + // noop + } + return { + error, + ...result, + }; + }; + + return { + connect: (options) => request({ ...options, method: 'CONNECT' }), + delete: (options) => request({ ...options, method: 'DELETE' }), + get: (options) => request({ ...options, method: 'GET' }), + getConfig, + head: (options) => request({ ...options, method: 'HEAD' }), + interceptors, + options: (options) => request({ ...options, method: 'OPTIONS' }), + patch: (options) => request({ ...options, method: 'PATCH' }), + post: (options) => request({ ...options, method: 'POST' }), + put: (options) => request({ ...options, method: 'PUT' }), + request, + trace: (options) => request({ ...options, method: 'TRACE' }), + }; +}; + +export const client = createClient({ + ...globalConfig, + // @ts-ignore + root: true, +}); diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/types.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/types.ts.snap new file mode 100644 index 000000000..13955fb64 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/types.ts.snap @@ -0,0 +1,163 @@ +import type { + BodySerializer, + Middleware, + QuerySerializer, + QuerySerializerOptions, +} from './utils'; + +type OmitKeys = Pick>; + +export interface Config + extends Omit { + /** + * Base URL for all requests made by this client. + * @default '' + */ + baseUrl?: string; + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: + | RequestInit['body'] + | Record + | Array> + | Array + | number; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * @default globalThis.fetch + */ + fetch?: (request: Request) => ReturnType; + /** + * By default, options passed to this call will be applied to the global + * client instance. Set to false to create a local client instance. + * @default true + */ + global?: boolean; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: + | 'CONNECT' + | 'DELETE' + | 'GET' + | 'HEAD' + | 'OPTIONS' + | 'PATCH' + | 'POST' + | 'PUT' + | 'TRACE'; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * @default 'auto' + */ + parseAs?: Exclude | 'auto' | 'stream'; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function for transforming response data before it's returned to the + * caller function. This is an ideal place to post-process server data, + * e.g. convert date ISO strings into native Date objects. + */ + responseTransformer?: (data: unknown) => Promise; +} + +interface RequestOptionsBase extends Omit { + path?: Record; + query?: Record; + url: string; +} + +export type RequestResult = Promise< + ({ data: Data; error: undefined } | { data: undefined; error: Error }) & { + request: Request; + response: Response; + } +>; + +type MethodFn = ( + options: RequestOptionsBase, +) => RequestResult; +type RequestFn = ( + options: RequestOptionsBase & Pick, 'method'>, +) => RequestResult; + +interface ClientBase { + connect: MethodFn; + delete: MethodFn; + get: MethodFn; + getConfig: () => Config; + head: MethodFn; + interceptors: Middleware; + options: MethodFn; + patch: MethodFn; + post: MethodFn; + put: MethodFn; + request: RequestFn; + trace: MethodFn; +} + +export type RequestOptions = RequestOptionsBase & + Config & { + headers: Headers; + }; + +export type Client = ClientBase; + +type OptionsBase = Omit & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; +}; + +export type Options = T extends { body?: any } + ? T extends { headers?: any } + ? OmitKeys & T + : OmitKeys & + T & + Pick + : T extends { headers?: any } + ? OmitKeys & + T & + Pick + : OptionsBase & T; diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/utils.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/utils.ts.snap new file mode 100644 index 000000000..b7f026ce0 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/core/utils.ts.snap @@ -0,0 +1,552 @@ +import type { Config } from './types'; + +interface PathSerializer { + path: Record; + url: string; +} + +const PATH_PARAM_RE = /\{[^{}]+\}/g; + +type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: Record; +}) => { + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + url = url.replace( + match, + style === 'label' ? `.${value as string}` : (value as string), + ); + } + } + return url; +}; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + let search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + search = [ + ...search, + serializeArrayParam({ + allowReserved, + explode: true, + name, + style: 'form', + value, + ...array, + }), + ]; + continue; + } + + if (typeof value === 'object') { + search = [ + ...search, + serializeObjectParam({ + allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...object, + }), + ]; + continue; + } + + search = [ + ...search, + serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }), + ]; + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + content: string | null, +): Exclude => { + if (!content) { + return; + } + + if (content === 'application/json' || content.endsWith('+json')) { + return 'json'; + } + + if (content === 'multipart/form-data') { + return 'formData'; + } + + if ( + [ + 'application/octet-stream', + 'application/pdf', + 'application/zip', + 'audio/', + 'image/', + 'video/', + ].some((type) => content.includes(type)) + ) { + return 'blob'; + } + + if (content.includes('text/')) { + return 'text'; + } +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = baseUrl + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +) => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header || typeof header !== 'object') { + continue; + } + + const iterator = + header instanceof Headers ? header.entries() : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + _fns: Interceptor[]; + + constructor() { + this._fns = []; + } + + eject(fn: Interceptor) { + const index = this._fns.indexOf(fn); + if (index !== -1) { + this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]; + } + } + + use(fn: Interceptor) { + this._fns = [...this._fns, fn]; + } +} + +// `createInterceptors()` response, meant for external use as it does not +// expose internals +export interface Middleware { + request: Pick>, 'eject' | 'use'>; + response: Pick< + Interceptors>, + 'eject' | 'use' + >; +} + +// do not add `Middleware` as return type so we can use _fns internally +export const createInterceptors = () => ({ + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ) => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T) => JSON.stringify(body), +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +) => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ) => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data; + }, +}; + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createDefaultConfig = (): Config => ({ + ...jsonBodySerializer, + baseUrl: '', + fetch: globalThis.fetch, + global: true, + headers: defaultHeaders, + querySerializer: defaultQuerySerializer, +}); diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/index.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/index.ts.snap new file mode 100644 index 000000000..0a2b84bae --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/index.ts.snap @@ -0,0 +1,4 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './schemas.gen'; +export * from './services.gen'; +export * from './types.gen'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/schemas.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/schemas.gen.ts.snap new file mode 100644 index 000000000..befaae022 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/schemas.gen.ts.snap @@ -0,0 +1,96 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export const $SimpleModel = { + description: 'This is a model that contains a some dates', + type: 'object', + required: ['id', 'name', 'enabled', 'modified'], + properties: { + id: { + type: 'number' + }, + name: { + maxLength: 255, + type: 'string' + }, + enabled: { + type: 'boolean', + readOnly: true + } + } +} as const; + +export const $ModelWithDates = { + description: 'This is a model that contains a some dates', + type: 'object', + required: ['id', 'name', 'enabled', 'modified'], + properties: { + id: { + type: 'number' + }, + name: { + maxLength: 255, + type: 'string' + }, + enabled: { + type: 'boolean', + readOnly: true + }, + modified: { + type: 'string', + format: 'date-time', + readOnly: true + }, + expires: { + type: 'string', + format: 'date', + readOnly: true + } + } +} as const; + +export const $ParentModelWithDates = { + description: 'This is a model that contains a some dates and arrays', + type: 'object', + required: ['id', 'name'], + properties: { + id: { + type: 'number' + }, + modified: { + type: 'string', + format: 'date-time', + readOnly: true + }, + items: { + type: 'array', + items: { + '$ref': '#/components/schemas/ModelWithDates' + } + }, + item: { + '$ref': '#/components/schemas/ModelWithDates' + }, + simpleItems: { + type: 'array', + items: { + '$ref': '#/components/schemas/SimpleModel' + } + }, + simpleItem: { + '$ref': '#/components/schemas/SimpleModel' + }, + dates: { + type: 'array', + items: { + type: 'string', + format: 'date-time' + } + }, + strings: { + type: 'array', + items: { + type: 'string' + } + } + } +} as const; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/services.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/services.gen.ts.snap new file mode 100644 index 000000000..59dd5adb0 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/services.gen.ts.snap @@ -0,0 +1,37 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { client, type Options } from './client'; +import { type ParentModelWithDatesError, type ParentModelWithDatesResponse, type ModelWithDatesError, type ModelWithDatesResponse, type ModelWithDatesArrayError, type ModelWithDatesArrayResponse, type ArrayOfDatesError, type ArrayOfDatesResponse, type DateError, type DateResponse, type MultipleResponsesError, type MultipleResponsesResponse, ParentModelWithDatesResponseTransformer, ModelWithDatesResponseTransformer, ModelWithDatesArrayResponseTransformer } from './types.gen'; + +export const parentModelWithDates = (options?: Options) => { return (options?.client ?? client).post({ + ...options, + url: '/api/model-with-dates', + responseTransformer: ParentModelWithDatesResponseTransformer +}); }; + +export const modelWithDates = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/model-with-dates', + responseTransformer: ModelWithDatesResponseTransformer +}); }; + +export const modelWithDatesArray = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/model-with-dates-array', + responseTransformer: ModelWithDatesArrayResponseTransformer +}); }; + +export const arrayOfDates = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/array-of-dates' +}); }; + +export const date = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/date' +}); }; + +export const multipleResponses = (options?: Options) => { return (options?.client ?? client).put({ + ...options, + url: '/api/multiple-responses' +}); }; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/types.gen.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/types.gen.ts.snap new file mode 100644 index 000000000..95bd3b551 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3_hey-api_client-fetch_inline_transform/types.gen.ts.snap @@ -0,0 +1,183 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * This is a model that contains a some dates + */ +export type SimpleModel = { + id: number; + name: string; + readonly enabled: boolean; +}; + +/** + * This is a model that contains a some dates + */ +export type ModelWithDates = { + id: number; + name: string; + readonly enabled: boolean; + readonly modified: Date; + readonly expires?: Date; +}; + +/** + * This is a model that contains a some dates and arrays + */ +export type ParentModelWithDates = { + id: number; + readonly modified?: Date; + items?: Array; + item?: ModelWithDates; + simpleItems?: Array; + simpleItem?: SimpleModel; + dates?: Array<(Date)>; + strings?: Array<(string)>; +}; + +export type ParentModelWithDatesResponse = ParentModelWithDates | unknown; + +export type ParentModelWithDatesError = unknown; + +export type ModelWithDatesResponse = ModelWithDates; + +export type ModelWithDatesError = unknown; + +export type ModelWithDatesArrayResponse = Array; + +export type ModelWithDatesArrayError = unknown; + +export type ArrayOfDatesResponse = Array<(Date)>; + +export type ArrayOfDatesError = unknown; + +export type DateResponse = Date; + +export type DateError = unknown; + +export type MultipleResponsesResponse = Array | Array; + +export type MultipleResponsesError = unknown; + +export type $OpenApiTs = { + '/api/model-with-dates': { + post: { + res: { + /** + * Success + */ + '200': ParentModelWithDates; + /** + * Success + */ + '201': unknown; + }; + }; + put: { + res: { + /** + * Success + */ + '200': ModelWithDates; + }; + }; + }; + '/api/model-with-dates-array': { + put: { + res: { + /** + * Success + */ + '200': Array; + }; + }; + }; + '/api/array-of-dates': { + put: { + res: { + /** + * Success + */ + '200': Array<(Date)>; + }; + }; + }; + '/api/date': { + put: { + res: { + /** + * Success + */ + '200': Date; + }; + }; + }; + '/api/multiple-responses': { + put: { + res: { + /** + * Updated + */ + '200': Array; + /** + * Created + */ + '201': Array; + }; + }; + }; +}; + +export type ParentModelWithDatesResponseTransformer = (data: any) => Promise; + +export type ParentModelWithDatesModelResponseTransformer = (data: any) => ParentModelWithDates; + +export type ModelWithDatesModelResponseTransformer = (data: any) => ModelWithDates; + +export const ModelWithDatesModelResponseTransformer: ModelWithDatesModelResponseTransformer = data => { + if (data?.modified) { + data.modified = new Date(data.modified); + } + if (data?.expires) { + data.expires = new Date(data.expires); + } + return data; +}; + +export const ParentModelWithDatesModelResponseTransformer: ParentModelWithDatesModelResponseTransformer = data => { + if (data?.modified) { + data.modified = new Date(data.modified); + } + if (Array.isArray(data?.items)) { + data.items.forEach(ModelWithDatesModelResponseTransformer); + } + if (data?.item) { + ModelWithDatesModelResponseTransformer(data.item); + } + if (Array.isArray(data?.dates)) { + data.dates = data.dates.map(item => new Date(item)); + } + return data; +}; + +export const ParentModelWithDatesResponseTransformer: ParentModelWithDatesResponseTransformer = async (data) => { + if (data) { + ParentModelWithDatesModelResponseTransformer(data); + } + return data; +}; + +export type ModelWithDatesResponseTransformer = (data: any) => Promise; + +export const ModelWithDatesResponseTransformer: ModelWithDatesResponseTransformer = async (data) => { + ModelWithDatesModelResponseTransformer(data); + return data; +}; + +export type ModelWithDatesArrayResponseTransformer = (data: any) => Promise; + +export const ModelWithDatesArrayResponseTransformer: ModelWithDatesArrayResponseTransformer = async (data) => { + if (Array.isArray(data)) { + data.forEach(ModelWithDatesModelResponseTransformer); + } + return data; +}; \ No newline at end of file diff --git a/packages/openapi-ts/test/index.spec.ts b/packages/openapi-ts/test/index.spec.ts index 5399c83a6..0885b6a98 100644 --- a/packages/openapi-ts/test/index.spec.ts +++ b/packages/openapi-ts/test/index.spec.ts @@ -113,9 +113,19 @@ describe('OpenAPI v3', () => { config: createConfig({ client: '@hey-api/client-fetch', }), - description: 'generate axios client', + description: 'generate Fetch API client', name: 'v3_hey-api_client-fetch', }, + { + config: createConfig({ + client: { + inline: true, + name: '@hey-api/client-fetch', + }, + }), + description: 'generate Fetch API client without external dependencies', + name: 'v3_hey-api_client-fetch_inline', + }, { config: createConfig({ client: '@hey-api/client-axios', diff --git a/packages/openapi-ts/test/sample.cjs b/packages/openapi-ts/test/sample.cjs index fe1d36325..14b18d457 100644 --- a/packages/openapi-ts/test/sample.cjs +++ b/packages/openapi-ts/test/sample.cjs @@ -3,7 +3,10 @@ const path = require('node:path'); const main = async () => { /** @type {import('../src/node/index').UserConfig} */ const config = { - client: '@hey-api/client-fetch', + client: { + inline: true, + name: '@hey-api/client-fetch', + }, debug: true, // input: './test/spec/v3-transforms.json', input: './test/spec/v3.json', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 510fcc934..5b983031a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,9 @@ importers: '@angular/router': specifier: 17.3.9 version: 17.3.9(@angular/common@17.3.9(@angular/core@17.3.9(rxjs@7.8.1)(zone.js@0.14.7))(rxjs@7.8.1))(@angular/core@17.3.9(rxjs@7.8.1)(zone.js@0.14.7))(@angular/platform-browser@17.3.9(@angular/animations@17.3.9(@angular/core@17.3.9(rxjs@7.8.1)(zone.js@0.14.7)))(@angular/common@17.3.9(@angular/core@17.3.9(rxjs@7.8.1)(zone.js@0.14.7))(rxjs@7.8.1))(@angular/core@17.3.9(rxjs@7.8.1)(zone.js@0.14.7)))(rxjs@7.8.1) + '@hey-api/client-fetch': + specifier: workspace:* + version: link:../client-fetch '@rollup/plugin-json': specifier: 6.1.0 version: 6.1.0(rollup@4.18.0)