Skip to content

Commit

Permalink
Merge pull request #376 from hey-api/feat/services-to-compiler-2
Browse files Browse the repository at this point in the history
feat: convert service request option templates to use compiler api
  • Loading branch information
jordanshatford authored Apr 13, 2024
2 parents d82f311 + 5e4fc6a commit 030bab7
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 164 deletions.
3 changes: 1 addition & 2 deletions packages/openapi-ts/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@ export function handlebarsPlugin(): Plugin {
knownHelpers: {
camelCase: true,
dataDestructure: true,
dataParameters: true,
equals: true,
escapeDescription: true,
ifdef: true,
nameOperationDataType: true,
notEquals: true,
toOperationComment: true,
toRequestOptions: true,
useDateType: true,
},
knownHelpersOnly: true,
Expand Down
56 changes: 45 additions & 11 deletions packages/openapi-ts/src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,28 @@ import { addLeadingComment, type Comments, isType, ots } from './utils';
/**
* Convert an unknown value to an expression.
* @param value - the unknown value.
* @param unescape - if string should be unescaped.
* @param shorthand - if shorthand syntax is allowed.
* @param indentifier - list of keys that are treated as indentifiers.
* @returns ts.Expression
*/
export const toExpression = (value: unknown, unescape = false): ts.Expression | undefined => {
const toExpression = <T = unknown>({
value,
unescape = false,
shorthand = false,
identifiers = [],
}: {
value: T;
unescape?: boolean;
shorthand?: boolean;
identifiers?: string[];
}): ts.Expression | undefined => {
if (Array.isArray(value)) {
return createArrayType({ arr: value });
}

if (typeof value === 'object' && value !== null) {
return createObjectType({ obj: value });
return createObjectType({ identifiers, obj: value, shorthand });
}

if (typeof value === 'number') {
Expand Down Expand Up @@ -47,46 +60,67 @@ export const createArrayType = <T>({
multiLine?: boolean;
}): ts.ArrayLiteralExpression =>
ts.factory.createArrayLiteralExpression(
arr.map(v => toExpression(v)).filter(isType<ts.Expression>),
arr.map(value => toExpression({ value })).filter(isType<ts.Expression>),
// Multiline if the array contains objects, or if specified by the user.
(!Array.isArray(arr[0]) && typeof arr[0] === 'object') || multiLine
);

/**
* Create Object type expression.
* @param options - options to use when creating type.
* @param comments - comments to add to each property.
* @param identifier - keys that should be treated as identifiers.
* @param multiLine - if the object should be multiline.
* @param obj - the object to create expression with.
* @param shorthand - if shorthand syntax should be used.
* @param unescape - if properties strings should be unescaped.
* @returns ts.ObjectLiteralExpression
*/
export const createObjectType = <T extends object>({
comments = {},
identifiers = [],
multiLine = true,
obj,
shorthand = false,
unescape = false,
}: {
obj: T;
comments?: Record<string | number, Comments>;
identifiers?: string[];
multiLine?: boolean;
shorthand?: boolean;
unescape?: boolean;
comments?: Record<string | number, Comments>;
}): ts.ObjectLiteralExpression => {
const properties = Object.entries(obj)
.map(([key, value]) => {
const initializer = toExpression(value, unescape);
// Pass all object properties as identifiers if the whole object is a indentifier
let initializer: ts.Expression | undefined = toExpression({
identifiers: identifiers.includes(key) ? Object.keys(value) : [],
shorthand,
unescape,
value,
});
if (!initializer) {
return undefined;
}
// Create a identifier if the current key is one and it is not an object
if (identifiers.includes(key) && !ts.isObjectLiteralExpression(initializer)) {
initializer = ts.factory.createIdentifier(value as string);
}
if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) {
key = `'${key}'`;
}
const assignment = ts.factory.createPropertyAssignment(key, initializer);
const assignment =
shorthand && key === value
? ts.factory.createShorthandPropertyAssignment(key)
: ts.factory.createPropertyAssignment(key, initializer);
const c = comments?.[key];
if (c?.length) {
addLeadingComment(assignment, c);
}
return assignment;
})
.filter(isType<ts.PropertyAssignment>);
const expression = ts.factory.createObjectLiteralExpression(properties, multiLine);
return expression;
.filter(isType<ts.ShorthandPropertyAssignment | ts.PropertyAssignment>);
return ts.factory.createObjectLiteralExpression(properties as any[], multiLine);
};

/**
Expand All @@ -112,7 +146,7 @@ export const createEnumDeclaration = <T extends object>({
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(name),
Object.entries(obj).map(([key, value]) => {
const initializer = toExpression(value, true);
const initializer = toExpression({ unescape: true, value });
const assignment = ts.factory.createEnumMember(key, initializer);
const c = comments?.[key];
if (c) {
Expand Down
62 changes: 8 additions & 54 deletions packages/openapi-ts/src/templates/exportService.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -19,73 +19,27 @@ export class {{{name}}}{{{@root.$config.postfixServices}}} {
{{#equals @root.$config.client 'angular'}}
public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> {
{{{dataDestructure this}}}
return this.httpRequest.request({
return this.httpRequest.request({{{toRequestOptions this}}});
}
{{else}}
public {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> {
{{{dataDestructure this}}}
return this.httpRequest.request({
return this.httpRequest.request({{{toRequestOptions this}}});
}
{{/equals}}
{{else}}
{{#equals @root.$config.client 'angular'}}
public {{{name}}}({{{nameOperationDataType 'req' this}}}): Observable<{{{nameOperationDataType 'res' this}}}> {
{{{dataDestructure this}}}
return __request(OpenAPI, this.http, {
return __request(OpenAPI, this.http, {{{toRequestOptions this}}});
}
{{else}}
public static {{{name}}}({{{nameOperationDataType 'req' this}}}): CancelablePromise<{{{nameOperationDataType 'res' this}}}> {
{{{dataDestructure this}}}
return __request(OpenAPI, {
return __request(OpenAPI, {{{toRequestOptions this}}});
}
{{/equals}}
{{/if}}
method: '{{{method}}}',
url: '{{{path}}}',
{{#if parametersPath}}
path: {
{{{dataParameters parametersPath}}}
},
{{/if}}
{{#if parametersCookie}}
cookies: {
{{{dataParameters parametersCookie}}}
},
{{/if}}
{{#if parametersHeader}}
headers: {
{{{dataParameters parametersHeader}}}
},
{{/if}}
{{#if parametersQuery}}
query: {
{{{dataParameters parametersQuery}}}
},
{{/if}}
{{#if parametersForm}}
formData: {
{{{dataParameters parametersForm}}}
},
{{/if}}
{{#if parametersBody}}
{{#equals parametersBody.in 'formData'}}
formData: {{{parametersBody.name}}},
{{/equals}}
{{#equals parametersBody.in 'body'}}
body: {{{parametersBody.name}}},
{{/equals}}
{{#if parametersBody.mediaType}}
mediaType: '{{{parametersBody.mediaType}}}',
{{/if}}
{{/if}}
{{#if responseHeader}}
responseHeader: '{{{responseHeader}}}',
{{/if}}
{{#if errors}}
errors: {
{{#each errors}}
{{{code}}}: `{{{escapeDescription description}}}`,
{{/each}}
},
{{/if}}
});
}

{{/each}}
}
2 changes: 0 additions & 2 deletions packages/openapi-ts/src/utils/__tests__/handlebars.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ describe('registerHandlebarHelpers', () => {
const helpers = Object.keys(Handlebars.helpers);
expect(helpers).toContain('camelCase');
expect(helpers).toContain('dataDestructure');
expect(helpers).toContain('dataParameters');
expect(helpers).toContain('equals');
expect(helpers).toContain('escapeDescription');
expect(helpers).toContain('ifdef');
expect(helpers).toContain('nameOperationDataType');
expect(helpers).toContain('notEquals');
Expand Down
83 changes: 66 additions & 17 deletions packages/openapi-ts/src/utils/handlebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,6 @@ const dataDestructure = (operation: Operation) => {
return '';
};

const dataParameters = (parameters: OperationParameter[]) => {
const output = parameters.map(parameter => {
const key = parameter.prop;
const value = parameter.name;
if (key === value) {
return key;
}
if (escapeName(key) === key) {
return `${key}: ${value}`;
}
return `'${key}': ${value}`;
});
return output.join(', ');
};

export const serviceExportedNamespace = () => '$OpenApiTs';

export const nameOperationDataType = (namespace: 'req' | 'res', operation: Service['operations'][number]) => {
Expand Down Expand Up @@ -135,7 +120,6 @@ export const nameOperationDataType = (namespace: 'req' | 'res', operation: Servi
export const registerHandlebarHelpers = (): void => {
Handlebars.registerHelper('camelCase', camelCase);
Handlebars.registerHelper('dataDestructure', dataDestructure);
Handlebars.registerHelper('dataParameters', dataParameters);

Handlebars.registerHelper(
'equals',
Expand All @@ -144,7 +128,72 @@ export const registerHandlebarHelpers = (): void => {
}
);

Handlebars.registerHelper('escapeDescription', escapeDescription);
Handlebars.registerHelper('toRequestOptions', (operation: Operation) => {
const toObj = (parameters: OperationParameter[]) =>
parameters.reduce(
(prev, curr) => {
const key = curr.prop;
const value = curr.name;
if (key === value) {
prev[key] = key;
} else if (escapeName(key) === key) {
prev[key] = value;
} else {
prev[`'${key}'`] = value;
}
return prev;
},
{} as Record<string, unknown>
);

const obj: Record<string, any> = {
method: operation.method,
url: operation.path,
};
if (operation.parametersPath.length) {
obj.path = toObj(operation.parametersPath);
}
if (operation.parametersCookie.length) {
obj.cookies = toObj(operation.parametersCookie);
}
if (operation.parametersHeader.length) {
obj.headers = toObj(operation.parametersHeader);
}
if (operation.parametersQuery.length) {
obj.query = toObj(operation.parametersQuery);
}
if (operation.parametersForm.length) {
obj.formData = toObj(operation.parametersForm);
}
if (operation.parametersBody) {
if (operation.parametersBody.in === 'formData') {
obj.formData = operation.parametersBody.name;
}
if (operation.parametersBody.in === 'body') {
obj.body = operation.parametersBody.name;
}
}
if (operation.parametersBody?.mediaType) {
obj.mediaType = operation.parametersBody?.mediaType;
}
if (operation.responseHeader) {
obj.responseHeader = operation.responseHeader;
}
if (operation.errors.length) {
const errors: Record<number, string> = {};
operation.errors.forEach(err => {
errors[err.code] = escapeDescription(err.description);
});
obj.errors = errors;
}
return compiler.utils.toString(
compiler.types.object({
identifiers: ['body', 'headers', 'formData', 'cookies', 'path', 'query'],
obj,
shorthand: true,
})
);
});

Handlebars.registerHelper('toOperationComment', (operation: Operation) => {
const config = getConfig();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,9 @@ export class ResponseService {
method: 'POST',
url: '/api/v{api-version}/response',
errors: {
500: `Message for 500 error`,
501: `Message for 501 error`,
502: `Message for 502 error`,
500: 'Message for 500 error',
501: 'Message for 501 error',
502: 'Message for 502 error',
},
});
}
Expand All @@ -393,9 +393,9 @@ export class ResponseService {
method: 'PUT',
url: '/api/v{api-version}/response',
errors: {
500: `Message for 500 error`,
501: `Message for 501 error`,
502: `Message for 502 error`,
500: 'Message for 500 error',
501: 'Message for 501 error',
502: 'Message for 502 error',
},
});
}
Expand Down Expand Up @@ -547,8 +547,8 @@ export class ComplexService {
parameterReference,
},
errors: {
400: `400 server error`,
500: `500 server error`,
400: '400 server error',
500: '500 server error',
},
});
}
Expand All @@ -567,8 +567,8 @@ export class HeaderService {
url: '/api/v{api-version}/header',
responseHeader: 'operation-location',
errors: {
400: `400 server error`,
500: `500 server error`,
400: '400 server error',
500: '500 server error',
},
});
}
Expand All @@ -590,10 +590,10 @@ export class ErrorService {
status,
},
errors: {
500: `Custom message: Internal Server Error`,
501: `Custom message: Not Implemented`,
502: `Custom message: Bad Gateway`,
503: `Custom message: Service Unavailable`,
500: 'Custom message: Internal Server Error',
501: 'Custom message: Not Implemented',
502: 'Custom message: Bad Gateway',
503: 'Custom message: Service Unavailable',
},
});
}
Expand Down
Loading

0 comments on commit 030bab7

Please sign in to comment.