Skip to content

Commit

Permalink
Merge pull request #246 from hey-api/fix/services-duplicates
Browse files Browse the repository at this point in the history
fix(openapi-ts): namespace service data types
  • Loading branch information
mrlubos authored Apr 4, 2024
2 parents 5ab6ab0 + 0335b3d commit f7a4a44
Show file tree
Hide file tree
Showing 19 changed files with 2,847 additions and 2,540 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-cars-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": patch
---

fix(openapi-ts): namespace service data types
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,5 @@
"devDependencies": {
"@changesets/cli": "2.27.1",
"@svitejs/changesets-changelog-github-compact": "1.1.0"
},
"packageManager": "[email protected]+sha256.01c01eeb990e379b31ef19c03e9d06a14afa5250b82e81303f88721c99ff2e6f"
}
}
1 change: 0 additions & 1 deletion packages/openapi-ts/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export function handlebarsPlugin(): Plugin {
modelUnionType: true,
nameOperationDataType: true,
notEquals: true,
operationDataType: true,
useDateType: true,
},
knownHelpersOnly: true,
Expand Down
14 changes: 13 additions & 1 deletion packages/openapi-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { writeClient } from './utils/write/client';

type Dependencies = Record<string, unknown>;

// Mapping of all dependencies used in each client. These should be installed in the generated client package
// Dependencies used in each client. User must have installed these to use the generated client
const clientDependencies: Record<Config['client'], string[]> = {
angular: ['@angular/common', '@angular/core', 'rxjs'],
axios: ['axios'],
Expand Down Expand Up @@ -127,6 +127,18 @@ const getConfig = async (userConfig: UserConfig, dependencies: Dependencies) =>
throw new Error('🚫 output must be within the current working directory');
}

if (postfixModels) {
console.warn(
'⚠️ Deprecation warning: postfixModels. This setting will be removed in future versions. Please create an issue wih your use case if you need this option https://github.com/hey-api/openapi-ts/issues'
);
}

if (postfixServices && postfixServices !== 'Service') {
console.warn(
'⚠️ Deprecation warning: postfixServices. This setting will be removed in future versions. Please create an issue wih your use case if you need this option https://github.com/hey-api/openapi-ts/issues'
);
}

if (!useOptions) {
console.warn(
'⚠️ Deprecation warning: useOptions set to false. This setting will be removed in future versions. Please migrate useOptions to true https://github.com/hey-api/openapi-ts#v0.27.38'
Expand Down
2 changes: 0 additions & 2 deletions packages/openapi-ts/src/templates/exportService.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{{{operationDataType this}}}

{{#equals @root.$config.client 'angular'}}
@Injectable({
providedIn: 'root',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{{#if @root.$config.useOptions~}}
{{~#if @root.$config.name~}}
{{#if parameters}}data: {{{nameOperationDataType name}}}{{#ifOperationDataOptional parameters}} = {}{{/ifOperationDataOptional}}{{/if}}
{{#if parameters}}data: {{{nameOperationDataType @root this}}}{{#ifOperationDataOptional parameters}} = {}{{/ifOperationDataOptional}}{{/if}}
{{~else~}}
{{~#notEquals @root.$config.serviceResponse 'generics'~}}
{{#if parameters}}data: {{{nameOperationDataType name}}}{{#ifOperationDataOptional parameters}} = {}{{/ifOperationDataOptional}}{{/if}}
{{#if parameters}}data: {{{nameOperationDataType @root this}}}{{#ifOperationDataOptional parameters}} = {}{{/ifOperationDataOptional}}{{/if}}
{{~else~}}
data: {{#if parameters}}{{{nameOperationDataType name}}} & {{/if}}TConfig<T>{{#ifOperationDataOptional parameters}} = {}{{/ifOperationDataOptional}}
data: {{#if parameters}}{{{nameOperationDataType @root this}}} & {{/if}}TConfig<T>{{#ifOperationDataOptional parameters}} = {}{{/ifOperationDataOptional}}
{{~/notEquals~}}
{{~/if~}}
{{~else}}
Expand Down
1 change: 0 additions & 1 deletion packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ describe('registerHandlebarHelpers', () => {
expect(helpers).toContain('modelUnionType');
expect(helpers).toContain('nameOperationDataType');
expect(helpers).toContain('notEquals');
expect(helpers).toContain('operationDataType');
expect(helpers).toContain('useDateType');
});
});
Expand Down
92 changes: 50 additions & 42 deletions packages/openapi-ts/src/utils/handlebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,41 +129,30 @@ const dataParameters = (config: Config, parameters: OperationParameter[]) => {
// same as `>isRequired` partial
const isRequired = (model: Pick<Model, 'default' | 'isRequired'>) => (model.isRequired && !model.default ? '' : '?');

const nameOperationDataType = (value: string) => camelCase(['T', 'Data', value].join('-'), { pascalCase: true });
const nameOperationDataType = (service: Service, operation: Service['operations'][number]) => {
const namespace = `${camelCase(service.name, { pascalCase: true })}Data`;
const key = camelCase(operation.name, { pascalCase: true });
return `${namespace}.${key}`;
};

const operationDataType = (config: Config, service: Service) => {
export const operationDataType = (config: Config, service: Service) => {
if (!config.useOptions) {
return '';
}
const partialType = Handlebars.partials['type'];
const output = service.operations
.filter(operation => operation.parameters.length)
.map(operation => {
const name = nameOperationDataType(operation.name);
return `export type ${name} = {
${sortByName(operation.parameters)
.filter(parameter => {
if (!config.experimental) {
return true;
}
return parameter.in !== 'query';
})
.map(parameter => {
let comment: string[] = [];
if (parameter.description) {
comment = ['/**', ` * ${escapeComment(parameter.description)}`, ' */'];
}
return [
...comment,
`${parameter.name + isRequired(parameter)}: ${partialType({ $config: config, ...parameter })}`,
].join('\n');
})
.join('\n')}
${
config.experimental
? `
query${operation.parametersQuery.every(parameter => !parameter.isRequired) ? '?' : ''}: {
${sortByName(operation.parametersQuery)
const namespace = `${camelCase(service.name, { pascalCase: true })}Data`;
const output = `export type ${namespace} = {
${service.operations
.filter(operation => operation.parameters.length)
.map(
operation => `${camelCase(operation.name, { pascalCase: true })}: {
${sortByName(operation.parameters)
.filter(parameter => {
if (!config.experimental) {
return true;
}
return parameter.in !== 'query';
})
.map(parameter => {
let comment: string[] = [];
if (parameter.description) {
Expand All @@ -175,13 +164,31 @@ const operationDataType = (config: Config, service: Service) => {
].join('\n');
})
.join('\n')}
}
`
: ''
}
}`;
});
return output.join('\n');
${
config.experimental
? `
query${operation.parametersQuery.every(parameter => !parameter.isRequired) ? '?' : ''}: {
${sortByName(operation.parametersQuery)
.map(parameter => {
let comment: string[] = [];
if (parameter.description) {
comment = ['/**', ` * ${escapeComment(parameter.description)}`, ' */'];
}
return [
...comment,
`${parameter.name + isRequired(parameter)}: ${partialType({ $config: config, ...parameter })}`,
].join('\n');
})
.join('\n')}
}
`
: ''
}
};`
)
.join('\n')}
}`;
return output;
};

export const registerHandlebarHelpers = (config: Config, client: Client): void => {
Expand Down Expand Up @@ -275,7 +282,12 @@ export const registerHandlebarHelpers = (config: Config, client: Client): void =
}
);

Handlebars.registerHelper('nameOperationDataType', nameOperationDataType);
Handlebars.registerHelper(
'nameOperationDataType',
function (service: Service, operation: Service['operations'][number]) {
return nameOperationDataType(service, operation);
}
);

Handlebars.registerHelper(
'notEquals',
Expand All @@ -284,10 +296,6 @@ export const registerHandlebarHelpers = (config: Config, client: Client): void =
}
);

Handlebars.registerHelper('operationDataType', function (service: Service) {
return operationDataType(config, service);
});

Handlebars.registerHelper(
'useDateType',
function (this: unknown, config: Config, format: string | undefined, options: Handlebars.HelperOptions) {
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-ts/src/utils/write/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export const writeClientModels = async (
outputPath: string,
config: Config
): Promise<void> => {
// Dont create empty file
if (client.models.length === 0) {
if (!client.models.length) {
return;
}

// Generate a file with all models
const results: string[] = [];
for (const model of client.models) {
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi-ts/src/utils/write/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export const writeClientSchemas = async (
outputPath: string,
config: Config
): Promise<void> => {
// Dont create empty file
if (client.models.length === 0) {
if (!client.models.length) {
return;
}

// Generate file with all schemas
const results: string[] = [];
for (const model of client.models) {
Expand Down
71 changes: 38 additions & 33 deletions packages/openapi-ts/src/utils/write/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from 'node:path';

import type { Client } from '../../types/client';
import type { Config } from '../../types/config';
import type { Templates } from '../handlebars';
import { operationDataType, type Templates } from '../handlebars';
import { unique } from '../unique';

/**
Expand All @@ -19,64 +19,69 @@ export const writeClientServices = async (
outputPath: string,
config: Config
): Promise<void> => {
// Dont create empty file
if (client.services.length === 0) {
if (!client.services.length) {
return;
}
// Generate a file with all services.
const results: string[] = [];
const imports: string[] = [];

let imports: string[] = [];
let operationTypes: string[] = [];
let results: string[] = [];

for (const service of client.services) {
const result = templates.exports.service({
$config: config,
...service,
});
imports.push(...service.imports);
results.push(result);
}
// Import all models required by the services.
const uniqueImports = imports.filter(unique);
if (uniqueImports.length > 0) {
const importString = `import type { ${uniqueImports.join(',')} } from './models';`;
results.unshift(importString);
const operationDataTypes = operationDataType(config, service);
imports = [...imports, ...service.imports];
operationTypes = [...operationTypes, operationDataTypes];
results = [...results, result];
}

// Import required packages and core files.
const imports2: string[] = [];
const coreImports: string[] = [];
if (config.client === 'angular') {
imports2.push(`import { Injectable } from '@angular/core';`);
coreImports.push(`import { Injectable } from '@angular/core';`);
if (config.name === undefined) {
imports2.push(`import { HttpClient } from '@angular/common/http';`);
coreImports.push(`import { HttpClient } from '@angular/common/http';`);
}
imports2.push(`import type { Observable } from 'rxjs';`);
coreImports.push(`import type { Observable } from 'rxjs';`);
} else {
imports2.push(`import type { CancelablePromise } from './core/CancelablePromise';`);
coreImports.push(`import type { CancelablePromise } from './core/CancelablePromise';`);
}
if (config.serviceResponse === 'response') {
imports2.push(`import type { ApiResult } from './core/ApiResult;`);
coreImports.push(`import type { ApiResult } from './core/ApiResult;`);
}
if (config.name) {
if (config.client === 'angular') {
imports2.push(`import { BaseHttpRequest } from './core/BaseHttpRequest';`);
coreImports.push(`import { BaseHttpRequest } from './core/BaseHttpRequest';`);
} else {
imports2.push(`import type { BaseHttpRequest } from './core/BaseHttpRequest';`);
coreImports.push(`import type { BaseHttpRequest } from './core/BaseHttpRequest';`);
}
} else {
if (config.useOptions) {
if (config.serviceResponse === 'generics') {
imports2.push(`import { mergeOpenApiConfig, OpenAPI } from './core/OpenAPI';`);
imports2.push(`import { request as __request } from './core/request';`);
imports2.push(`import type { TApiResponse, TConfig, TResult } from './core/types';`);
coreImports.push(`import { mergeOpenApiConfig, OpenAPI } from './core/OpenAPI';`);
coreImports.push(`import { request as __request } from './core/request';`);
coreImports.push(`import type { TApiResponse, TConfig, TResult } from './core/types';`);
} else {
imports2.push(`import { OpenAPI } from './core/OpenAPI';`);
imports2.push(`import { request as __request } from './core/request';`);
coreImports.push(`import { OpenAPI } from './core/OpenAPI';`);
coreImports.push(`import { request as __request } from './core/request';`);
}
} else {
imports2.push(`import { OpenAPI } from './core/OpenAPI';`);
imports2.push(`import { request as __request } from './core/request';`);
coreImports.push(`import { OpenAPI } from './core/OpenAPI';`);
coreImports.push(`import { request as __request } from './core/request';`);
}
}
results.unshift(imports2.join('\n'));
// Generate index file exporting all generated service files.
const file = path.resolve(outputPath, 'services.ts');
await writeFileSync(file, results.join('\n\n'));

// Import all models required by the services.
let modelImportsString = '';
const uniqueImports = imports.filter(unique);
if (uniqueImports.length) {
modelImportsString = `import type { ${uniqueImports.join(',')} } from './models';`;
}

const data = [coreImports.join('\n'), modelImportsString, ...operationTypes, ...results].join('\n\n');

await writeFileSync(path.resolve(outputPath, 'services.ts'), data);
};
Loading

0 comments on commit f7a4a44

Please sign in to comment.