- Updated PR with master branch

This commit is contained in:
Ferdi Koomen 2022-01-25 17:29:55 +01:00
commit 9424a7b240
206 changed files with 2967 additions and 1996 deletions

View File

@ -7,3 +7,6 @@ trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
[*.hbs]
indent_style = tab

View File

@ -1,4 +1,5 @@
dist
samples
test/generated
test/e2e/generated
node_modules

21
CHANGELOG.md Normal file
View File

@ -0,0 +1,21 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.16.0] - 2021-01-25
### Added
- Added option to set the indentation (spaces and tabs)
- Added option to export separate client file that allows usage for multiple backends
### Fixed
- Decoupled OpenAPI object from requests
- Updated dependencies
## [0.15.0] - 2021-01-24
### Added
- Added change log and releases on GitHub
## [0.14.0] - 2021-01-24
### Fixed
- Added missing `postfix` options to typedef
- Updated escaping of comments and descriptions
- Better handling of services without tags
- Updated dependencies

View File

@ -41,13 +41,15 @@ $ openapi --help
-i, --input <value> OpenAPI specification, can be a path, url or string content (required)
-o, --output <value> Output directory (required)
-c, --client <value> HTTP client to generate [fetch, xhr, axios, node] (default: "fetch")
--name <value> Custom client class name
--useOptions Use options instead of arguments
--useUnionTypes Use union types instead of enums
--exportCore <value> Write core files to disk (default: true)
--exportServices <value> Write services to disk (default: true)
--exportModels <value> Write models to disk (default: true)
--exportSchemas <value> Write schemas to disk (default: false)
--postfix <value> Service name postfix (default: "Service")
--indent <value> Service name postfix (default: "Service")
--postfix <value> Indentation options [4, 2, tab] (default: "5")
--request <value> Path to custom request file
-h, --help display help for command
@ -95,6 +97,33 @@ OpenAPI.generate({
## Features
### Generate client instance with `--name` option
The OpenAPI generator allows creation of client instances to support the multiple backend services use case.
The generated client uses an instance of the server configuration and not the global `OpenAPI` constant.
To generate a client instance, set a custom name to the client class, use `--name` option.
```
openapi --input ./spec.json --output ./dist ---name AppClient
```
The generated client will be exported from the `index` file and can be used as shown below:
```typescript
// Create the client instance with server and authentication details
const appClient = new AppClient({
BASE: 'http://server-host.com',
TOKEN: '1234'
});
// Use the client instance to make the API call
const res = await appClient.organizations.createOrganization({
name: 'OrgName',
description: 'OrgDescription',
});
```
### Argument style vs. Object style `--useOptions`
There's no [named parameter](https://en.wikipedia.org/wiki/Named_parameter) in JavaScript or TypeScript, because of
that, we offer the flag `--useOptions` to generate code in two different styles.

View File

@ -13,12 +13,14 @@ const params = program
.requiredOption('-i, --input <value>', 'OpenAPI specification, can be a path, url or string content (required)')
.requiredOption('-o, --output <value>', 'Output directory (required)')
.option('-c, --client <value>', 'HTTP client to generate [fetch, xhr, node, axios, angular]', 'fetch')
.option('--name <value>', 'Custom client class name')
.option('--useOptions', 'Use options instead of arguments')
.option('--useUnionTypes', 'Use union types instead of enums')
.option('--exportCore <value>', 'Write core files to disk', true)
.option('--exportServices <value>', 'Write services to disk', true)
.option('--exportModels <value>', 'Write models to disk', true)
.option('--exportSchemas <value>', 'Write schemas to disk', false)
.option('--indent <value>', 'Indentation options [4, 2, tabs]', '4')
.option('--postfix <value>', 'Service name postfix', 'Service')
.option('--request <value>', 'Path to custom request file')
.parse(process.argv)
@ -31,12 +33,14 @@ if (OpenAPI) {
input: params.input,
output: params.output,
httpClient: params.client,
clientName: params.name,
useOptions: params.useOptions,
useUnionTypes: params.useUnionTypes,
exportCore: JSON.parse(params.exportCore) === true,
exportServices: JSON.parse(params.exportServices) === true,
exportModels: JSON.parse(params.exportModels) === true,
exportSchemas: JSON.parse(params.exportSchemas) === true,
indent: params.indent,
postfix: params.postfix,
request: params.request,
})

View File

@ -25,6 +25,11 @@ const config: Config.InitialOptions = {
'<rootDir>/test/e2e/v3.node.spec.ts',
'<rootDir>/test/e2e/v3.axios.spec.ts',
'<rootDir>/test/e2e/v3.babel.spec.ts',
'<rootDir>/test/e2e/client.fetch.spec.ts',
'<rootDir>/test/e2e/client.xhr.spec.ts',
'<rootDir>/test/e2e/client.node.spec.ts',
'<rootDir>/test/e2e/client.axios.spec.ts',
'<rootDir>/test/e2e/client.babel.spec.ts',
],
},
],

View File

@ -1,6 +1,6 @@
{
"name": "openapi-typescript-codegen",
"version": "0.13.0",
"version": "0.16.0",
"description": "Library that generates Typescript clients based on the OpenAPI specification.",
"author": "Ferdi Koomen",
"homepage": "https://github.com/ferdikoomen/openapi-typescript-codegen",
@ -77,7 +77,7 @@
"@types/express": "4.17.13",
"@types/glob": "7.2.0",
"@types/jest": "27.4.0",
"@types/node": "17.0.10",
"@types/node": "17.0.12",
"@types/node-fetch": "^2.5.12",
"@types/qs": "6.9.7",
"@typescript-eslint/eslint-plugin": "5.10.1",
@ -96,10 +96,10 @@
"jest-cli": "27.4.7",
"node-fetch": "^2.6.6",
"prettier": "2.5.1",
"puppeteer": "13.1.1",
"puppeteer": "13.1.2",
"qs": "6.10.3",
"rimraf": "^3.0.2",
"rollup": "2.66.0",
"rollup": "2.66.1",
"rollup-plugin-node-externals": "3.1.2",
"rollup-plugin-terser": "7.0.2",
"rxjs": "7.5.2",

View File

@ -36,6 +36,7 @@ const handlebarsPlugin = () => ({
enumerator: true,
escapeComment: true,
escapeDescription: true,
camelCase: true,
},
});
return `export default ${templateSpec};`;

5
src/Indent.ts Normal file
View File

@ -0,0 +1,5 @@
export enum Indent {
SPACE_4 = '4',
SPACE_2 = '2',
TAB = 'tab',
}

View File

@ -1,4 +1,5 @@
import { HttpClient } from './HttpClient';
import { Indent } from './Indent';
import { parse as parseV2 } from './openApi/v2';
import { parse as parseV3 } from './openApi/v3';
import { getOpenApiSpec } from './utils/getOpenApiSpec';
@ -9,17 +10,20 @@ import { registerHandlebarTemplates } from './utils/registerHandlebarTemplates';
import { writeClient } from './utils/writeClient';
export { HttpClient } from './HttpClient';
export { Indent } from './Indent';
export type Options = {
input: string | Record<string, any>;
output: string;
httpClient?: HttpClient;
clientName?: string;
useOptions?: boolean;
useUnionTypes?: boolean;
exportCore?: boolean;
exportServices?: boolean;
exportModels?: boolean;
exportSchemas?: boolean;
indent?: Indent;
postfix?: string;
request?: string;
write?: boolean;
@ -32,30 +36,34 @@ export type Options = {
* @param input The relative location of the OpenAPI spec
* @param output The relative location of the output directory
* @param httpClient The selected httpClient (fetch, xhr, node or axios)
* @param clientName Custom client class name
* @param useOptions Use options or arguments functions
* @param useUnionTypes Use union types instead of enums
* @param exportCore: Generate core client classes
* @param exportServices: Generate services
* @param exportModels: Generate models
* @param exportSchemas: Generate schemas
* @param postfix: Service name postfix
* @param request: Path to custom request file
* @param exportCore Generate core client classes
* @param exportServices Generate services
* @param exportModels Generate models
* @param exportSchemas Generate schemas
* @param indent Indentation options (4, 2 or tab)
* @param postfix Service name postfix
* @param request Path to custom request file
* @param write Write the files to disk (true or false)
*/
export async function generate({
export const generate = async ({
input,
output,
httpClient = HttpClient.FETCH,
clientName,
useOptions = false,
useUnionTypes = false,
exportCore = true,
exportServices = true,
exportModels = true,
exportSchemas = false,
indent = Indent.SPACE_4,
postfix = 'Service',
request,
write = true,
}: Options): Promise<void> {
}: Options): Promise<void> => {
const openApi = isString(input) ? await getOpenApiSpec(input) : input;
const openApiVersion = getOpenApiVersion(openApi);
const templates = registerHandlebarTemplates({
@ -80,7 +88,9 @@ export async function generate({
exportServices,
exportModels,
exportSchemas,
indent,
postfix,
clientName,
request
);
break;
@ -101,13 +111,15 @@ export async function generate({
exportServices,
exportModels,
exportSchemas,
indent,
postfix,
clientName,
request
);
break;
}
}
}
};
export default {
HttpClient,

View File

@ -10,11 +10,11 @@ import { getServiceVersion } from './parser/getServiceVersion';
* all the models, services and schema's we should output.
* @param openApi The OpenAPI spec that we have loaded from disk.
*/
export function parse(openApi: OpenApi): Client {
export const parse = (openApi: OpenApi): Client => {
const version = getServiceVersion(openApi.info.version);
const server = getServer(openApi);
const models = getModels(openApi);
const services = getServices(openApi);
return { version, server, models, services };
}
};

View File

@ -1,4 +1,4 @@
export function escapeName(value: string): string {
export const escapeName = (value: string): string => {
if (value) {
const validName = /^[a-zA-Z_$][\w$]+$/g.test(value);
if (!validName) {
@ -6,4 +6,4 @@ export function escapeName(value: string): string {
}
}
return value;
}
};

View File

@ -7,7 +7,7 @@ import type { WithEnumExtension } from '../interfaces/Extensions/WithEnumExtensi
* @param enumerators
* @param definition
*/
export function extendEnum(enumerators: Enum[], definition: WithEnumExtension): Enum[] {
export const extendEnum = (enumerators: Enum[], definition: WithEnumExtension): Enum[] => {
const names = definition['x-enum-varnames'];
const descriptions = definition['x-enum-descriptions'];
@ -17,4 +17,4 @@ export function extendEnum(enumerators: Enum[], definition: WithEnumExtension):
value: enumerator.value,
type: enumerator.type,
}));
}
};

View File

@ -1,7 +1,7 @@
import type { Enum } from '../../../client/interfaces/Enum';
import { isDefined } from '../../../utils/isDefined';
export function getEnum(values?: (string | number)[]): Enum[] {
export const getEnum = (values?: (string | number)[]): Enum[] => {
if (Array.isArray(values)) {
return values
.filter((value, index, arr) => {
@ -30,4 +30,4 @@ export function getEnum(values?: (string | number)[]): Enum[] {
});
}
return [];
}
};

View File

@ -3,7 +3,7 @@ import type { Enum } from '../../../client/interfaces/Enum';
/**
* @deprecated
*/
export function getEnumFromDescription(description: string): Enum[] {
export const getEnumFromDescription = (description: string): Enum[] => {
// Check if we can find this special format string:
// None=0,Something=1,AnotherThing=2
if (/^(\w+=[0-9]+)/g.test(description)) {
@ -36,4 +36,4 @@ export function getEnumFromDescription(description: string): Enum[] {
}
return [];
}
};

View File

@ -24,9 +24,9 @@ const TYPE_MAPPINGS = new Map<string, string>([
/**
* Get mapped type for given type to any basic Typescript/Javascript type.
*/
export function getMappedType(type: string, format?: string): string | undefined {
export const getMappedType = (type: string, format?: string): string | undefined => {
if (format === 'binary') {
return 'binary';
}
return TYPE_MAPPINGS.get(type);
}
};

View File

@ -9,12 +9,12 @@ import { getModelComposition } from './getModelComposition';
import { getModelProperties } from './getModelProperties';
import { getType } from './getType';
export function getModel(
export const getModel = (
openApi: OpenApi,
definition: OpenApiSchema,
isDefinition: boolean = false,
name: string = ''
): Model {
): Model => {
const model: Model = {
name,
export: 'interface',
@ -162,4 +162,4 @@ export function getModel(
}
return model;
}
};

View File

@ -9,13 +9,13 @@ import { getRequiredPropertiesFromComposition } from './getRequiredPropertiesFro
// Fix for circular dependency
export type GetModelFn = typeof getModel;
export function getModelComposition(
export const getModelComposition = (
openApi: OpenApi,
definition: OpenApiSchema,
definitions: OpenApiSchema[],
type: 'one-of' | 'any-of' | 'all-of',
getModel: GetModelFn
): ModelComposition {
): ModelComposition => {
const composition: ModelComposition = {
type,
imports: [],
@ -87,4 +87,4 @@ export function getModelComposition(
}
return composition;
}
};

View File

@ -9,7 +9,7 @@ import { getType } from './getType';
// Fix for circular dependency
export type GetModelFn = typeof getModel;
export function getModelProperties(openApi: OpenApi, definition: OpenApiSchema, getModel: GetModelFn): Model[] {
export const getModelProperties = (openApi: OpenApi, definition: OpenApiSchema, getModel: GetModelFn): Model[] => {
const models: Model[] = [];
for (const propertyName in definition.properties) {
if (definition.properties.hasOwnProperty(propertyName)) {
@ -85,4 +85,4 @@ export function getModelProperties(openApi: OpenApi, definition: OpenApiSchema,
}
}
return models;
}
};

View File

@ -6,6 +6,6 @@ import type { Type } from '../../../client/interfaces/Type';
* @param modelClass The parsed model class type.
* @returns The model template type (<T> or empty).
*/
export function getModelTemplate(modelClass: Type): string {
export const getModelTemplate = (modelClass: Type): string => {
return modelClass.template ? '<T>' : '';
}
};

View File

@ -3,7 +3,7 @@ import type { OpenApi } from '../interfaces/OpenApi';
import { getModel } from './getModel';
import { getType } from './getType';
export function getModels(openApi: OpenApi): Model[] {
export const getModels = (openApi: OpenApi): Model[] => {
const models: Model[] = [];
for (const definitionName in openApi.definitions) {
if (openApi.definitions.hasOwnProperty(definitionName)) {
@ -14,4 +14,4 @@ export function getModels(openApi: OpenApi): Model[] {
}
}
return models;
}
};

View File

@ -5,25 +5,22 @@ import type { OpenApiOperation } from '../interfaces/OpenApiOperation';
import { getOperationErrors } from './getOperationErrors';
import { getOperationName } from './getOperationName';
import { getOperationParameters } from './getOperationParameters';
import { getOperationPath } from './getOperationPath';
import { getOperationResponseHeader } from './getOperationResponseHeader';
import { getOperationResponses } from './getOperationResponses';
import { getOperationResults } from './getOperationResults';
import { getServiceName } from './getServiceName';
import { sortByRequired } from './sortByRequired';
export function getOperation(
export const getOperation = (
openApi: OpenApi,
url: string,
method: string,
tag: string,
op: OpenApiOperation,
pathParams: OperationParameters
): Operation {
): Operation => {
const serviceName = getServiceName(tag);
const operationNameFallback = `${method}${serviceName}`;
const operationName = getOperationName(op.operationId || operationNameFallback);
const operationPath = getOperationPath(url);
const operationName = getOperationName(op.operationId || `${method}`);
// Create a new operation object for this method.
const operation: Operation = {
@ -33,7 +30,7 @@ export function getOperation(
description: op.description || null,
deprecated: op.deprecated === true,
method: method.toUpperCase(),
path: operationPath,
path: url,
parameters: [...pathParams.parameters],
parametersPath: [...pathParams.parametersPath],
parametersQuery: [...pathParams.parametersQuery],
@ -76,4 +73,4 @@ export function getOperation(
operation.parameters = operation.parameters.sort(sortByRequired);
return operation;
}
};

View File

@ -5,7 +5,7 @@ import type { OperationResponse } from '../../../client/interfaces/OperationResp
*
* @param operationResponses
*/
export function getOperationErrors(operationResponses: OperationResponse[]): OperationError[] {
export const getOperationErrors = (operationResponses: OperationResponse[]): OperationError[] => {
return operationResponses
.filter(operationResponse => {
return operationResponse.code >= 300 && operationResponse.description;
@ -14,4 +14,4 @@ export function getOperationErrors(operationResponses: OperationResponse[]): Ope
code: response.code,
description: response.description!,
}));
}
};

View File

@ -5,10 +5,10 @@ import camelCase from 'camelcase';
* This converts the input string to camelCase, so the method name follows
* the most popular Javascript and Typescript writing style.
*/
export function getOperationName(value: string): string {
export const getOperationName = (value: string): string => {
const clean = value
.replace(/^[^a-zA-Z]+/g, '')
.replace(/[^\w\-]+/g, '-')
.trim();
return camelCase(clean);
}
};

View File

@ -12,7 +12,7 @@ import { getOperationParameterName } from './getOperationParameterName';
import { getRef } from './getRef';
import { getType } from './getType';
export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParameter): OperationParameter {
export const getOperationParameter = (openApi: OpenApi, parameter: OpenApiParameter): OperationParameter => {
const operationParameter: OperationParameter = {
in: parameter.in,
prop: parameter.name,
@ -147,4 +147,4 @@ export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParame
}
return operationParameter;
}
};

View File

@ -1,10 +1,10 @@
import type { OperationParameter } from '../../../client/interfaces/OperationParameter';
import type { OpenApiParameter } from '../interfaces/OpenApiParameter';
export function getOperationParameterDefault(
export const getOperationParameterDefault = (
parameter: OpenApiParameter,
operationParameter: OperationParameter
): string | undefined {
): string | undefined => {
if (parameter.default === undefined) {
return;
}
@ -39,4 +39,4 @@ export function getOperationParameterDefault(
}
return;
}
};

View File

@ -7,10 +7,10 @@ const reservedWords =
* Replaces any invalid characters from a parameter name.
* For example: 'filter.someProperty' becomes 'filterSomeProperty'.
*/
export function getOperationParameterName(value: string): string {
export const getOperationParameterName = (value: string): string => {
const clean = value
.replace(/^[^a-zA-Z]+/g, '')
.replace(/[^\w\-]+/g, '-')
.trim();
return camelCase(clean).replace(reservedWords, '_$1');
}
};

View File

@ -4,7 +4,7 @@ import type { OpenApiParameter } from '../interfaces/OpenApiParameter';
import { getOperationParameter } from './getOperationParameter';
import { getRef } from './getRef';
export function getOperationParameters(openApi: OpenApi, parameters: OpenApiParameter[]): OperationParameters {
export const getOperationParameters = (openApi: OpenApi, parameters: OpenApiParameter[]): OperationParameters => {
const operationParameters: OperationParameters = {
imports: [],
parameters: [],
@ -58,4 +58,4 @@ export function getOperationParameters(openApi: OpenApi, parameters: OpenApiPara
}
});
return operationParameters;
}
};

View File

@ -1,18 +0,0 @@
import { getOperationPath } from './getOperationPath';
describe('getOperationPath', () => {
it('should produce correct result', () => {
expect(getOperationPath('/api/v{api-version}/list/{id}/{type}')).toEqual(
'/api/v${OpenAPI.VERSION}/list/${id}/${type}'
);
expect(getOperationPath('/api/v{api-version}/list/{id}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}');
expect(getOperationPath('/api/v1/list/{id}')).toEqual('/api/v1/list/${id}');
expect(getOperationPath('/api/{foobar}')).toEqual('/api/${foobar}');
expect(getOperationPath('/api/{fooBar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{foo-bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{foo_bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{foo.bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{Foo-Bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{FOO-BAR}')).toEqual('/api/${fooBar}');
});
});

View File

@ -1,16 +0,0 @@
import { getOperationParameterName } from './getOperationParameterName';
/**
* Get the final service path, this replaces the "{api-version}" placeholder
* with a new template string placeholder so we can dynamically inject the
* OpenAPI version without the need to hardcode this in the URL.
* Plus we return the correct parameter names to replace in the URL.
* @param path
*/
export function getOperationPath(path: string): string {
return path
.replace(/\{(.*?)\}/g, (_, w: string) => {
return `\${${getOperationParameterName(w)}}`;
})
.replace('${apiVersion}', '${OpenAPI.VERSION}');
}

View File

@ -7,11 +7,11 @@ import { getModel } from './getModel';
import { getRef } from './getRef';
import { getType } from './getType';
export function getOperationResponse(
export const getOperationResponse = (
openApi: OpenApi,
response: OpenApiResponse,
responseCode: number
): OperationResponse {
): OperationResponse => {
const operationResponse: OperationResponse = {
in: 'response',
name: '',
@ -96,4 +96,4 @@ export function getOperationResponse(
}
return operationResponse;
}
};

View File

@ -1,4 +1,4 @@
export function getOperationResponseCode(value: string | 'default'): number | null {
export const getOperationResponseCode = (value: string | 'default'): number | null => {
// You can specify a "default" response, this is treated as HTTP code 200
if (value === 'default') {
return 200;
@ -13,4 +13,4 @@ export function getOperationResponseCode(value: string | 'default'): number | nu
}
return null;
}
};

View File

@ -1,6 +1,6 @@
import type { OperationResponse } from '../../../client/interfaces/OperationResponse';
export function getOperationResponseHeader(operationResponses: OperationResponse[]): string | null {
export const getOperationResponseHeader = (operationResponses: OperationResponse[]): string | null => {
const header = operationResponses.find(operationResponses => {
return operationResponses.in === 'header';
});
@ -8,4 +8,4 @@ export function getOperationResponseHeader(operationResponses: OperationResponse
return header.name;
}
return null;
}
};

View File

@ -6,7 +6,7 @@ import { getOperationResponse } from './getOperationResponse';
import { getOperationResponseCode } from './getOperationResponseCode';
import { getRef } from './getRef';
export function getOperationResponses(openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] {
export const getOperationResponses = (openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] => {
const operationResponses: OperationResponse[] = [];
// Iterate over each response code and get the
@ -28,4 +28,4 @@ export function getOperationResponses(openApi: OpenApi, responses: OpenApiRespon
return operationResponses.sort((a, b): number => {
return a.code < b.code ? -1 : a.code > b.code ? 1 : 0;
});
}
};

View File

@ -1,15 +1,15 @@
import type { Model } from '../../../client/interfaces/Model';
import type { OperationResponse } from '../../../client/interfaces/OperationResponse';
function areEqual(a: Model, b: Model): boolean {
const areEqual = (a: Model, b: Model): boolean => {
const equal = a.type === b.type && a.base === b.base && a.template === b.template;
if (equal && a.link && b.link) {
return areEqual(a.link, b.link);
}
return equal;
}
};
export function getOperationResults(operationResponses: OperationResponse[]): OperationResponse[] {
export const getOperationResults = (operationResponses: OperationResponse[]): OperationResponse[] => {
const operationResults: OperationResponse[] = [];
// Filter out success response codes, but skip "204 No Content"
@ -49,4 +49,4 @@ export function getOperationResults(operationResponses: OperationResponse[]): Op
}) === index
);
});
}
};

View File

@ -4,7 +4,7 @@ import type { OpenApiReference } from '../interfaces/OpenApiReference';
const ESCAPED_REF_SLASH = /~1/g;
const ESCAPED_REF_TILDE = /~0/g;
export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
export const getRef = <T>(openApi: OpenApi, item: T & OpenApiReference): T => {
if (item.$ref) {
// Fetch the paths to the definitions, this converts:
// "#/definitions/Form" to ["definitions", "Form"]
@ -29,4 +29,4 @@ export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
return result as T;
}
return item as T;
}
};

View File

@ -7,12 +7,12 @@ import { getRef } from './getRef';
// Fix for circular dependency
export type GetModelFn = typeof getModel;
export function getRequiredPropertiesFromComposition(
export const getRequiredPropertiesFromComposition = (
openApi: OpenApi,
required: string[],
definitions: OpenApiSchema[],
getModel: GetModelFn
): Model[] {
): Model[] => {
return definitions
.reduce((properties, definition) => {
if (definition.$ref) {
@ -30,4 +30,4 @@ export function getRequiredPropertiesFromComposition(
isRequired: true,
};
});
}
};

View File

@ -4,10 +4,10 @@ import type { OpenApi } from '../interfaces/OpenApi';
* Get the base server url.
* @param openApi
*/
export function getServer(openApi: OpenApi): string {
export const getServer = (openApi: OpenApi): string => {
const scheme = openApi.schemes?.[0] || 'http';
const host = openApi.host;
const basePath = openApi.basePath || '';
const url = host ? `${scheme}://${host}${basePath}` : basePath;
return url.replace(/\/$/g, '');
}
};

View File

@ -4,10 +4,10 @@ import camelCase from 'camelcase';
* Convert the input value to a correct service name. This converts
* the input string to PascalCase.
*/
export function getServiceName(value: string): string {
export const getServiceName = (value: string): string => {
const clean = value
.replace(/^[^a-zA-Z]+/g, '')
.replace(/[^\w\-]+/g, '-')
.trim();
return camelCase(clean, { pascalCase: true });
}
};

View File

@ -3,6 +3,6 @@
* This basically removes any "v" prefix from the version string.
* @param version
*/
export function getServiceVersion(version = '1.0'): string {
export const getServiceVersion = (version = '1.0'): string => {
return String(version).replace(/^v/gi, '');
}
};

View File

@ -26,6 +26,6 @@ describe('getServices', () => {
});
expect(services).toHaveLength(1);
expect(services[0].name).toEqual('');
expect(services[0].name).toEqual('Default');
});
});

View File

@ -7,7 +7,7 @@ import { getOperationParameters } from './getOperationParameters';
/**
* Get the OpenAPI services
*/
export function getServices(openApi: OpenApi): Service[] {
export const getServices = (openApi: OpenApi): Service[] => {
const services = new Map<string, Service>();
for (const url in openApi.paths) {
if (openApi.paths.hasOwnProperty(url)) {
@ -28,7 +28,7 @@ export function getServices(openApi: OpenApi): Service[] {
case 'patch':
// Each method contains an OpenAPI operation, we parse the operation
const op = path[method]!;
const tags = op.tags?.length ? op.tags.filter(unique) : [''];
const tags = op.tags?.length ? op.tags.filter(unique) : ['Default'];
tags.forEach(tag => {
const operation = getOperation(openApi, url, method, tag, op, pathParams);
@ -52,4 +52,4 @@ export function getServices(openApi: OpenApi): Service[] {
}
}
return Array.from(services.values());
}
};

View File

@ -2,16 +2,16 @@ import type { Type } from '../../../client/interfaces/Type';
import { getMappedType } from './getMappedType';
import { stripNamespace } from './stripNamespace';
function encode(value: string): string {
const encode = (value: string): string => {
return value.replace(/^[^a-zA-Z_$]+/g, '').replace(/[^\w$]+/g, '_');
}
};
/**
* Parse any string value into a type object.
* @param type String value like "integer" or "Link[Model]".
* @param format String value like "binary" or "date".
*/
export function getType(type: string = 'any', format?: string): Type {
export const getType = (type: string = 'any', format?: string): Type => {
const result: Type = {
type: 'any',
base: 'any',
@ -64,4 +64,4 @@ export function getType(type: string = 'any', format?: string): Type {
}
return result;
}
};

View File

@ -1,9 +1,9 @@
import type { OperationParameter } from '../../../client/interfaces/OperationParameter';
export function sortByRequired(a: OperationParameter, b: OperationParameter): number {
export const sortByRequired = (a: OperationParameter, b: OperationParameter): number => {
const aNeedsValue = a.isRequired && a.default === undefined;
const bNeedsValue = b.isRequired && b.default === undefined;
if (aNeedsValue && !bNeedsValue) return -1;
if (bNeedsValue && !aNeedsValue) return 1;
return 0;
}
};

View File

@ -2,11 +2,11 @@
* Strip (OpenAPI) namespaces fom values.
* @param value
*/
export function stripNamespace(value: string): string {
export const stripNamespace = (value: string): string => {
return value
.trim()
.replace(/^#\/definitions\//, '')
.replace(/^#\/parameters\//, '')
.replace(/^#\/responses\//, '')
.replace(/^#\/securityDefinitions\//, '');
}
};

View File

@ -10,11 +10,11 @@ import { getServiceVersion } from './parser/getServiceVersion';
* all the models, services and schema's we should output.
* @param openApi The OpenAPI spec that we have loaded from disk.
*/
export function parse(openApi: OpenApi): Client {
export const parse = (openApi: OpenApi): Client => {
const version = getServiceVersion(openApi.info.version);
const server = getServer(openApi);
const models = getModels(openApi);
const services = getServices(openApi);
return { version, server, models, services };
}
};

View File

@ -1,4 +1,4 @@
export function escapeName(value: string): string {
export const escapeName = (value: string): string => {
if (value) {
const validName = /^[a-zA-Z_$][\w$]+$/g.test(value);
if (!validName) {
@ -6,4 +6,4 @@ export function escapeName(value: string): string {
}
}
return value;
}
};

View File

@ -7,7 +7,7 @@ import type { WithEnumExtension } from '../interfaces/Extensions/WithEnumExtensi
* @param enumerators
* @param definition
*/
export function extendEnum(enumerators: Enum[], definition: WithEnumExtension): Enum[] {
export const extendEnum = (enumerators: Enum[], definition: WithEnumExtension): Enum[] => {
const names = definition['x-enum-varnames'];
const descriptions = definition['x-enum-descriptions'];
@ -17,4 +17,4 @@ export function extendEnum(enumerators: Enum[], definition: WithEnumExtension):
value: enumerator.value,
type: enumerator.type,
}));
}
};

View File

@ -21,7 +21,7 @@ const BASIC_MEDIA_TYPES = [
'multipart/batch',
];
export function getContent(openApi: OpenApi, content: Dictionary<OpenApiMediaType>): Content | null {
export const getContent = (openApi: OpenApi, content: Dictionary<OpenApiMediaType>): Content | null => {
const basicMediaTypeWithSchema = Object.keys(content)
.filter(mediaType => {
const cleanMediaType = mediaType.split(';')[0].trim();
@ -43,4 +43,4 @@ export function getContent(openApi: OpenApi, content: Dictionary<OpenApiMediaTyp
};
}
return null;
}
};

View File

@ -1,7 +1,7 @@
import type { Enum } from '../../../client/interfaces/Enum';
import { isDefined } from '../../../utils/isDefined';
export function getEnum(values?: (string | number)[]): Enum[] {
export const getEnum = (values?: (string | number)[]): Enum[] => {
if (Array.isArray(values)) {
return values
.filter((value, index, arr) => {
@ -30,4 +30,4 @@ export function getEnum(values?: (string | number)[]): Enum[] {
});
}
return [];
}
};

View File

@ -3,7 +3,7 @@ import type { Enum } from '../../../client/interfaces/Enum';
/**
* @deprecated
*/
export function getEnumFromDescription(description: string): Enum[] {
export const getEnumFromDescription = (description: string): Enum[] => {
// Check if we can find this special format string:
// None=0,Something=1,AnotherThing=2
if (/^(\w+=[0-9]+)/g.test(description)) {
@ -36,4 +36,4 @@ export function getEnumFromDescription(description: string): Enum[] {
}
return [];
}
};

View File

@ -24,9 +24,9 @@ const TYPE_MAPPINGS = new Map<string, string>([
/**
* Get mapped type for given type to any basic Typescript/Javascript type.
*/
export function getMappedType(type: string, format?: string): string | undefined {
export const getMappedType = (type: string, format?: string): string | undefined => {
if (format === 'binary') {
return 'binary';
}
return TYPE_MAPPINGS.get(type);
}
};

View File

@ -10,12 +10,12 @@ import { getModelDefault } from './getModelDefault';
import { getModelProperties } from './getModelProperties';
import { getType } from './getType';
export function getModel(
export const getModel = (
openApi: OpenApi,
definition: OpenApiSchema,
isDefinition: boolean = false,
name: string = ''
): Model {
): Model => {
const model: Model = {
name,
export: 'interface',
@ -191,4 +191,4 @@ export function getModel(
}
return model;
}
};

View File

@ -9,13 +9,13 @@ import { getRequiredPropertiesFromComposition } from './getRequiredPropertiesFro
// Fix for circular dependency
export type GetModelFn = typeof getModel;
export function getModelComposition(
export const getModelComposition = (
openApi: OpenApi,
definition: OpenApiSchema,
definitions: OpenApiSchema[],
type: 'one-of' | 'any-of' | 'all-of',
getModel: GetModelFn
): ModelComposition {
): ModelComposition => {
const composition: ModelComposition = {
type,
imports: [],
@ -87,4 +87,4 @@ export function getModelComposition(
}
return composition;
}
};

View File

@ -1,7 +1,7 @@
import type { Model } from '../../../client/interfaces/Model';
import type { OpenApiSchema } from '../interfaces/OpenApiSchema';
export function getModelDefault(definition: OpenApiSchema, model?: Model): string | undefined {
export const getModelDefault = (definition: OpenApiSchema, model?: Model): string | undefined => {
if (definition.default === undefined) {
return;
}
@ -36,4 +36,4 @@ export function getModelDefault(definition: OpenApiSchema, model?: Model): strin
}
return;
}
};

View File

@ -10,12 +10,12 @@ import { getType } from './getType';
// Fix for circular dependency
export type GetModelFn = typeof getModel;
export function getModelProperties(
export const getModelProperties = (
openApi: OpenApi,
definition: OpenApiSchema,
getModel: GetModelFn,
parent?: Model
): Model[] {
): Model[] => {
const models: Model[] = [];
const discriminator = findOneOfParentDiscriminator(openApi, parent);
for (const propertyName in definition.properties) {
@ -104,4 +104,4 @@ export function getModelProperties(
}
return models;
}
};

View File

@ -6,6 +6,6 @@ import type { Type } from '../../../client/interfaces/Type';
* @param modelClass The parsed model class type.
* @returns The model template type (<T> or empty).
*/
export function getModelTemplate(modelClass: Type): string {
export const getModelTemplate = (modelClass: Type): string => {
return modelClass.template ? '<T>' : '';
}
};

View File

@ -3,7 +3,7 @@ import type { OpenApi } from '../interfaces/OpenApi';
import { getModel } from './getModel';
import { getType } from './getType';
export function getModels(openApi: OpenApi): Model[] {
export const getModels = (openApi: OpenApi): Model[] => {
const models: Model[] = [];
if (openApi.components) {
for (const definitionName in openApi.components.schemas) {
@ -16,4 +16,4 @@ export function getModels(openApi: OpenApi): Model[] {
}
}
return models;
}
};

View File

@ -6,7 +6,6 @@ import type { OpenApiRequestBody } from '../interfaces/OpenApiRequestBody';
import { getOperationErrors } from './getOperationErrors';
import { getOperationName } from './getOperationName';
import { getOperationParameters } from './getOperationParameters';
import { getOperationPath } from './getOperationPath';
import { getOperationRequestBody } from './getOperationRequestBody';
import { getOperationResponseHeader } from './getOperationResponseHeader';
import { getOperationResponses } from './getOperationResponses';
@ -15,18 +14,16 @@ import { getRef } from './getRef';
import { getServiceName } from './getServiceName';
import { sortByRequired } from './sortByRequired';
export function getOperation(
export const getOperation = (
openApi: OpenApi,
url: string,
method: string,
tag: string,
op: OpenApiOperation,
pathParams: OperationParameters
): Operation {
): Operation => {
const serviceName = getServiceName(tag);
const operationNameFallback = `${method}${serviceName}`;
const operationName = getOperationName(op.operationId || operationNameFallback);
const operationPath = getOperationPath(url);
const operationName = getOperationName(op.operationId || `${method}`);
// Create a new operation object for this method.
const operation: Operation = {
@ -36,7 +33,7 @@ export function getOperation(
description: op.description || null,
deprecated: op.deprecated === true,
method: method.toUpperCase(),
path: operationPath,
path: url,
parameters: [...pathParams.parameters],
parametersPath: [...pathParams.parametersPath],
parametersQuery: [...pathParams.parametersQuery],
@ -87,4 +84,4 @@ export function getOperation(
operation.parameters = operation.parameters.sort(sortByRequired);
return operation;
}
};

View File

@ -1,7 +1,7 @@
import type { OperationError } from '../../../client/interfaces/OperationError';
import type { OperationResponse } from '../../../client/interfaces/OperationResponse';
export function getOperationErrors(operationResponses: OperationResponse[]): OperationError[] {
export const getOperationErrors = (operationResponses: OperationResponse[]): OperationError[] => {
return operationResponses
.filter(operationResponse => {
return operationResponse.code >= 300 && operationResponse.description;
@ -10,4 +10,4 @@ export function getOperationErrors(operationResponses: OperationResponse[]): Ope
code: response.code,
description: response.description!,
}));
}
};

View File

@ -5,10 +5,10 @@ import camelCase from 'camelcase';
* This converts the input string to camelCase, so the method name follows
* the most popular Javascript and Typescript writing style.
*/
export function getOperationName(value: string): string {
export const getOperationName = (value: string): string => {
const clean = value
.replace(/^[^a-zA-Z]+/g, '')
.replace(/[^\w\-]+/g, '-')
.trim();
return camelCase(clean);
}
};

View File

@ -9,7 +9,7 @@ import { getOperationParameterName } from './getOperationParameterName';
import { getRef } from './getRef';
import { getType } from './getType';
export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParameter): OperationParameter {
export const getOperationParameter = (openApi: OpenApi, parameter: OpenApiParameter): OperationParameter => {
const operationParameter: OperationParameter = {
in: parameter.in,
prop: parameter.name,
@ -89,4 +89,4 @@ export function getOperationParameter(openApi: OpenApi, parameter: OpenApiParame
}
return operationParameter;
}
};

View File

@ -7,10 +7,10 @@ const reservedWords =
* Replaces any invalid characters from a parameter name.
* For example: 'filter.someProperty' becomes 'filterSomeProperty'.
*/
export function getOperationParameterName(value: string): string {
export const getOperationParameterName = (value: string): string => {
const clean = value
.replace(/^[^a-zA-Z]+/g, '')
.replace(/[^\w\-]+/g, '-')
.trim();
return camelCase(clean).replace(reservedWords, '_$1');
}
};

View File

@ -4,7 +4,7 @@ import type { OpenApiParameter } from '../interfaces/OpenApiParameter';
import { getOperationParameter } from './getOperationParameter';
import { getRef } from './getRef';
export function getOperationParameters(openApi: OpenApi, parameters: OpenApiParameter[]): OperationParameters {
export const getOperationParameters = (openApi: OpenApi, parameters: OpenApiParameter[]): OperationParameters => {
const operationParameters: OperationParameters = {
imports: [],
parameters: [],
@ -58,4 +58,4 @@ export function getOperationParameters(openApi: OpenApi, parameters: OpenApiPara
}
});
return operationParameters;
}
};

View File

@ -1,18 +0,0 @@
import { getOperationPath } from './getOperationPath';
describe('getOperationPath', () => {
it('should produce correct result', () => {
expect(getOperationPath('/api/v{api-version}/list/{id}/{type}')).toEqual(
'/api/v${OpenAPI.VERSION}/list/${id}/${type}'
);
expect(getOperationPath('/api/v{api-version}/list/{id}')).toEqual('/api/v${OpenAPI.VERSION}/list/${id}');
expect(getOperationPath('/api/v1/list/{id}')).toEqual('/api/v1/list/${id}');
expect(getOperationPath('/api/{foobar}')).toEqual('/api/${foobar}');
expect(getOperationPath('/api/{fooBar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{foo-bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{foo_bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{foo.bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{Foo-Bar}')).toEqual('/api/${fooBar}');
expect(getOperationPath('/api/{FOO-BAR}')).toEqual('/api/${fooBar}');
});
});

View File

@ -1,16 +0,0 @@
import { getOperationParameterName } from './getOperationParameterName';
/**
* Get the final service path, this replaces the "{api-version}" placeholder
* with a new template string placeholder so we can dynamically inject the
* OpenAPI version without the need to hardcode this in the URL.
* Plus we return the correct parameter names to replace in the URL.
* @param path
*/
export function getOperationPath(path: string): string {
return path
.replace(/\{(.*?)\}/g, (_, w: string) => {
return `\${${getOperationParameterName(w)}}`;
})
.replace('${apiVersion}', '${OpenAPI.VERSION}');
}

View File

@ -6,7 +6,7 @@ import { getContent } from './getContent';
import { getModel } from './getModel';
import { getType } from './getType';
export function getOperationRequestBody(openApi: OpenApi, body: OpenApiRequestBody): OperationParameter {
export const getOperationRequestBody = (openApi: OpenApi, body: OpenApiRequestBody): OperationParameter => {
const requestBody: OperationParameter = {
in: 'body',
export: 'interface',
@ -83,4 +83,4 @@ export function getOperationRequestBody(openApi: OpenApi, body: OpenApiRequestBo
}
return requestBody;
}
};

View File

@ -8,11 +8,11 @@ import { getModel } from './getModel';
import { getRef } from './getRef';
import { getType } from './getType';
export function getOperationResponse(
export const getOperationResponse = (
openApi: OpenApi,
response: OpenApiResponse,
responseCode: number
): OperationResponse {
): OperationResponse => {
const operationResponse: OperationResponse = {
in: 'response',
name: '',
@ -95,4 +95,4 @@ export function getOperationResponse(
}
return operationResponse;
}
};

View File

@ -1,4 +1,4 @@
export function getOperationResponseCode(value: string | 'default'): number | null {
export const getOperationResponseCode = (value: string | 'default'): number | null => {
// You can specify a "default" response, this is treated as HTTP code 200
if (value === 'default') {
return 200;
@ -13,4 +13,4 @@ export function getOperationResponseCode(value: string | 'default'): number | nu
}
return null;
}
};

View File

@ -1,6 +1,6 @@
import type { OperationResponse } from '../../../client/interfaces/OperationResponse';
export function getOperationResponseHeader(operationResponses: OperationResponse[]): string | null {
export const getOperationResponseHeader = (operationResponses: OperationResponse[]): string | null => {
const header = operationResponses.find(operationResponses => {
return operationResponses.in === 'header';
});
@ -8,4 +8,4 @@ export function getOperationResponseHeader(operationResponses: OperationResponse
return header.name;
}
return null;
}
};

View File

@ -6,7 +6,7 @@ import { getOperationResponse } from './getOperationResponse';
import { getOperationResponseCode } from './getOperationResponseCode';
import { getRef } from './getRef';
export function getOperationResponses(openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] {
export const getOperationResponses = (openApi: OpenApi, responses: OpenApiResponses): OperationResponse[] => {
const operationResponses: OperationResponse[] = [];
// Iterate over each response code and get the
@ -28,4 +28,4 @@ export function getOperationResponses(openApi: OpenApi, responses: OpenApiRespon
return operationResponses.sort((a, b): number => {
return a.code < b.code ? -1 : a.code > b.code ? 1 : 0;
});
}
};

View File

@ -1,15 +1,15 @@
import type { Model } from '../../../client/interfaces/Model';
import type { OperationResponse } from '../../../client/interfaces/OperationResponse';
function areEqual(a: Model, b: Model): boolean {
const areEqual = (a: Model, b: Model): boolean => {
const equal = a.type === b.type && a.base === b.base && a.template === b.template;
if (equal && a.link && b.link) {
return areEqual(a.link, b.link);
}
return equal;
}
};
export function getOperationResults(operationResponses: OperationResponse[]): OperationResponse[] {
export const getOperationResults = (operationResponses: OperationResponse[]): OperationResponse[] => {
const operationResults: OperationResponse[] = [];
// Filter out success response codes, but skip "204 No Content"
@ -49,4 +49,4 @@ export function getOperationResults(operationResponses: OperationResponse[]): Op
}) === index
);
});
}
};

View File

@ -4,7 +4,7 @@ import type { OpenApiReference } from '../interfaces/OpenApiReference';
const ESCAPED_REF_SLASH = /~1/g;
const ESCAPED_REF_TILDE = /~0/g;
export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
export const getRef = <T>(openApi: OpenApi, item: T & OpenApiReference): T => {
if (item.$ref) {
// Fetch the paths to the definitions, this converts:
// "#/components/schemas/Form" to ["components", "schemas", "Form"]
@ -29,4 +29,4 @@ export function getRef<T>(openApi: OpenApi, item: T & OpenApiReference): T {
return result as T;
}
return item as T;
}
};

View File

@ -7,12 +7,12 @@ import { getRef } from './getRef';
// Fix for circular dependency
export type GetModelFn = typeof getModel;
export function getRequiredPropertiesFromComposition(
export const getRequiredPropertiesFromComposition = (
openApi: OpenApi,
required: string[],
definitions: OpenApiSchema[],
getModel: GetModelFn
): Model[] {
): Model[] => {
return definitions
.reduce((properties, definition) => {
if (definition.$ref) {
@ -30,4 +30,4 @@ export function getRequiredPropertiesFromComposition(
isRequired: true,
};
});
}
};

View File

@ -1,6 +1,6 @@
import type { OpenApi } from '../interfaces/OpenApi';
export function getServer(openApi: OpenApi): string {
export const getServer = (openApi: OpenApi): string => {
const server = openApi.servers?.[0];
const variables = server?.variables || {};
let url = server?.url || '';
@ -10,4 +10,4 @@ export function getServer(openApi: OpenApi): string {
}
}
return url.replace(/\/$/g, '');
}
};

View File

@ -4,10 +4,10 @@ import camelCase from 'camelcase';
* Convert the input value to a correct service name. This converts
* the input string to PascalCase.
*/
export function getServiceName(value: string): string {
export const getServiceName = (value: string): string => {
const clean = value
.replace(/^[^a-zA-Z]+/g, '')
.replace(/[^\w\-]+/g, '-')
.trim();
return camelCase(clean, { pascalCase: true });
}
};

View File

@ -3,6 +3,6 @@
* This basically removes any "v" prefix from the version string.
* @param version
*/
export function getServiceVersion(version = '1.0'): string {
export const getServiceVersion = (version = '1.0'): string => {
return String(version).replace(/^v/gi, '');
}
};

View File

@ -26,6 +26,6 @@ describe('getServices', () => {
});
expect(services).toHaveLength(1);
expect(services[0].name).toEqual('');
expect(services[0].name).toEqual('Default');
});
});

View File

@ -7,7 +7,7 @@ import { getOperationParameters } from './getOperationParameters';
/**
* Get the OpenAPI services
*/
export function getServices(openApi: OpenApi): Service[] {
export const getServices = (openApi: OpenApi): Service[] => {
const services = new Map<string, Service>();
for (const url in openApi.paths) {
if (openApi.paths.hasOwnProperty(url)) {
@ -28,7 +28,7 @@ export function getServices(openApi: OpenApi): Service[] {
case 'patch':
// Each method contains an OpenAPI operation, we parse the operation
const op = path[method]!;
const tags = op.tags?.length ? op.tags.filter(unique) : [''];
const tags = op.tags?.length ? op.tags.filter(unique) : ['Default'];
tags.forEach(tag => {
const operation = getOperation(openApi, url, method, tag, op, pathParams);
@ -52,4 +52,4 @@ export function getServices(openApi: OpenApi): Service[] {
}
}
return Array.from(services.values());
}
};

View File

@ -3,16 +3,16 @@ import { isDefined } from '../../../utils/isDefined';
import { getMappedType } from './getMappedType';
import { stripNamespace } from './stripNamespace';
function encode(value: string): string {
const encode = (value: string): string => {
return value.replace(/^[^a-zA-Z_$]+/g, '').replace(/[^\w$]+/g, '_');
}
};
/**
* Parse any string value into a type object.
* @param type String or String[] value like "integer", "Link[Model]" or ["string", "null"].
* @param format String value like "binary" or "date".
*/
export function getType(type: string | string[] = 'any', format?: string): Type {
export const getType = (type: string | string[] = 'any', format?: string): Type => {
const result: Type = {
type: 'any',
base: 'any',
@ -79,4 +79,4 @@ export function getType(type: string | string[] = 'any', format?: string): Type
}
return result;
}
};

View File

@ -1,9 +1,9 @@
import type { OperationParameter } from '../../../client/interfaces/OperationParameter';
export function sortByRequired(a: OperationParameter, b: OperationParameter): number {
export const sortByRequired = (a: OperationParameter, b: OperationParameter): number => {
const aNeedsValue = a.isRequired && a.default === undefined;
const bNeedsValue = b.isRequired && b.default === undefined;
if (aNeedsValue && !bNeedsValue) return -1;
if (bNeedsValue && !aNeedsValue) return 1;
return 0;
}
};

View File

@ -2,7 +2,7 @@
* Strip (OpenAPI) namespaces fom values.
* @param value
*/
export function stripNamespace(value: string): string {
export const stripNamespace = (value: string): string => {
return value
.trim()
.replace(/^#\/components\/schemas\//, '')
@ -14,4 +14,4 @@ export function stripNamespace(value: string): string {
.replace(/^#\/components\/securitySchemes\//, '')
.replace(/^#\/components\/links\//, '')
.replace(/^#\/components\/callbacks\//, '');
}
};

View File

@ -1,7 +1,7 @@
export default {
compiler: [8, '>= 4.3.0'],
useData: true,
main: function () {
main: () => {
return '';
},
};

40
src/templates/client.hbs Normal file
View File

@ -0,0 +1,40 @@
{{>header}}
import type { BaseHttpRequest } from './core/BaseHttpRequest';
import type { OpenAPIConfig } from './core/OpenAPI';
import { {{{httpRequest}}} } from './core/{{{httpRequest}}}';
{{#if services}}
{{#each services}}
import { {{{name}}}{{{@root.postfix}}} } from './services/{{{name}}}{{{@root.postfix}}}';
{{/each}}
{{/if}}
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class {{{clientName}}} {
{{#each services}}
public readonly {{{camelCase name}}}: {{{name}}}{{{@root.postfix}}};
{{/each}}
private readonly request: BaseHttpRequest;
constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = {{{httpRequest}}}) {
this.request = new HttpRequest({
BASE: config?.BASE ?? '{{{server}}}',
VERSION: config?.VERSION ?? '{{{version}}}',
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? 'include',
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
});
{{#each services}}
this.{{{camelCase name}}} = new {{{name}}}{{{@root.postfix}}}(this.request);
{{/each}}
}
}

View File

@ -3,18 +3,18 @@
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
constructor(response: ApiResult, message: string) {
super(message);
constructor(response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
}
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
}
}

View File

@ -1,14 +1,15 @@
{{>header}}
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly path: string;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};

View File

@ -1,9 +1,9 @@
{{>header}}
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};

View File

@ -0,0 +1,18 @@
{{>header}}
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export class BaseHttpRequest {
protected readonly config: OpenAPIConfig;
constructor(config: OpenAPIConfig) {
this.config = config;
}
public request<T>(options: ApiRequestOptions): CancelablePromise<T> {
throw new Error('Not Implemented');
}
}

View File

@ -2,126 +2,126 @@
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
readonly [Symbol.toStringTag]: string;
readonly [Symbol.toStringTag]: string;
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
this.#resolve?.(value);
};
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
this.#resolve?.(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
this.#reject?.(reason);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
this.#reject?.(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
this.#reject?.(new CancelError('Request aborted'));
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
this.#reject?.(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}

View File

@ -0,0 +1,24 @@
{{>header}}
import type { ApiRequestOptions } from './ApiRequestOptions';
import { BaseHttpRequest } from './BaseHttpRequest';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
import { request as __request } from './request';
export class {{httpRequest}} extends BaseHttpRequest {
constructor(config: OpenAPIConfig) {
super(config);
}
/**
* Request method
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
public request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return __request(this.config, options);
}
}

View File

@ -5,26 +5,26 @@ import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
type Config = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string>;
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string>;
USERNAME?: string | Resolver<string>;
PASSWORD?: string | Resolver<string>;
HEADERS?: Headers | Resolver<Headers>;
ENCODE_PATH?: (path: string) => string;
};
export const OpenAPI: Config = {
BASE: '{{{server}}}',
VERSION: '{{{version}}}',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
export const OpenAPI: OpenAPIConfig = {
BASE: '{{{server}}}',
VERSION: '{{{version}}}',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};

View File

@ -1,42 +1,42 @@
async function getHeaders(options: ApiRequestOptions): Promise<HttpHeaders> {
const token = await resolve(options, OpenAPI.TOKEN);
const username = await resolve(options, OpenAPI.USERNAME);
const password = await resolve(options, OpenAPI.PASSWORD);
const additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const additionalHeaders = await resolve(options, config.HEADERS);
const defaultHeaders = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
const defaultHeaders = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
const headers = new HttpHeaders(defaultHeaders);
const headers = new Headers(defaultHeaders);
if (isStringWithValue(token)) {
headers.append('Authorization', `Bearer ${token}`);
}
if (isStringWithValue(token)) {
headers.append('Authorization', `Bearer ${token}`);
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers.append('Authorization', `Basic ${credentials}`);
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers.append('Authorization', `Basic ${credentials}`);
}
if (options.body) {
if (options.mediaType) {
headers.append('Content-Type', options.mediaType);
} else if (isBlob(options.body)) {
headers.append('Content-Type', options.body.type || 'application/octet-stream');
} else if (isString(options.body)) {
headers.append('Content-Type', 'text/plain');
} else if (!isFormData(options.body)) {
headers.append('Content-Type', 'application/json');
}
}
if (options.body) {
if (options.mediaType) {
headers.append('Content-Type', options.mediaType);
} else if (isBlob(options.body)) {
headers.append('Content-Type', options.body.type || 'application/octet-stream');
} else if (isString(options.body)) {
headers.append('Content-Type', 'text/plain');
} else if (!isFormData(options.body)) {
headers.append('Content-Type', 'application/json');
}
}
return headers;
}
return headers;
};

View File

@ -1,12 +1,12 @@
function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return;
}
const getRequestBody = (options: ApiRequestOptions): BodyInit | undefined => {
if (options.body) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return;
};

View File

@ -1,18 +1,18 @@
async function getResponseBody(response: Response): Promise<any> {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const isJSON = contentType.toLowerCase().startsWith('application/json');
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return;
}
const getResponseBody = async (response: Response): Promise<any> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const isJSON = contentType.toLowerCase().startsWith('application/json');
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return;
};

View File

@ -1,9 +1,9 @@
function getResponseHeader(response: Response, responseHeader?: string): string | undefined {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return;
}
const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return;
};

View File

@ -8,7 +8,7 @@ import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import { OpenAPI } from './OpenAPI';
import type { OpenAPIConfig } from './OpenAPI';
{{>functions/isDefined}}
@ -59,39 +59,39 @@ import { OpenAPI } from './OpenAPI';
/**
* Request using fetch client
* @param http The Angular HTTP client
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export function request<T>(http: HttpClient, options: ApiRequestOptions): Observable<T> {
return new CancelablePromise<T>(async (resolve, reject, onCancel) => {
try {
const url = getUrl(options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(options);
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, formData, body, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, formData, body, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader || responseBody,
};
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrors(options, result);
catchErrors(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
}
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

View File

@ -1,25 +1,26 @@
async function sendRequest(
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: BodyInit | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> {
const controller = new AbortController();
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
formData: FormData | undefined,
body: BodyInit | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> => {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body || formData,
method: options.method,
signal: controller.signal,
};
const request: RequestInit = {
headers,
body: body || formData,
method: options.method,
signal: controller.signal,
};
if (OpenAPI.WITH_CREDENTIALS) {
request.credentials = OpenAPI.CREDENTIALS;
}
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
onCancel(() => controller.abort());
onCancel(() => controller.abort());
return await fetch(url, request);
}
return await fetch(url, request);
};

View File

@ -1,30 +1,30 @@
async function getHeaders(options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> {
const token = await resolve(options, OpenAPI.TOKEN);
const username = await resolve(options, OpenAPI.USERNAME);
const password = await resolve(options, OpenAPI.PASSWORD);
const additionalHeaders = await resolve(options, OpenAPI.HEADERS);
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const additionalHeaders = await resolve(options, config.HEADERS);
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
...formHeaders,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
...formHeaders,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
return headers;
}
return headers;
};

View File

@ -1,6 +1,6 @@
function getRequestBody(options: ApiRequestOptions): any {
if (options.body) {
return options.body;
}
return;
}
const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body) {
return options.body;
}
return;
};

View File

@ -1,6 +1,6 @@
function getResponseBody(response: AxiosResponse<any>): any {
if (response.status !== 204) {
return response.data;
}
return;
}
const getResponseBody = (response: AxiosResponse<any>): any => {
if (response.status !== 204) {
return response.data;
}
return;
};

Some files were not shown because too many files have changed in this diff Show More