From 077c53a2ec821acd0185341611a7df1e0d38196c Mon Sep 17 00:00:00 2001 From: Declan Vong Date: Mon, 11 May 2020 17:14:04 +1000 Subject: [PATCH 1/2] resource methods: deduplicate request types between regular and body methods --- src/app.ts | 149 ++++++++++++++++++++++++++++----------------------- src/utils.ts | 19 ++++--- 2 files changed, 94 insertions(+), 74 deletions(-) diff --git a/src/app.ts b/src/app.ts index b96ba64d8d..19b3a78a55 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,17 +1,19 @@ +import { promisify } from 'bluebird'; +import sortObject from 'deep-sort-object'; import fs from 'fs'; +import lineReader from 'line-reader'; import _ from 'lodash'; -import path, {basename, join, resolve} from 'path'; +import path, { basename, join, resolve } from 'path'; import request from 'request'; -import sortObject from 'deep-sort-object'; -import lineReader from 'line-reader'; -import {promisify} from 'bluebird'; +import { Template } from './template'; import { + camelCaseParts, ensureDirectoryExists, getResourceTypeName, + getTypeName, parseVersion, } from './utils'; -import {StreamWriter, TextWriter} from './writer'; -import {Template} from './template'; +import { StreamWriter, TextWriter } from './writer'; type JsonSchema = gapi.client.discovery.JsonSchema; type RestResource = gapi.client.discovery.RestResource; @@ -417,10 +419,11 @@ function getMethodReturn( const name = schemas['Request'] ? 'client.Request' : 'Request'; if (method.response) { - const schema = schemas[checkExists(method.response.$ref)]; + const schemaName = method.response.$ref; + const schema = schemas[checkExists(schemaName)]; - if (schema && !_.isEmpty(schema.properties)) { - return `${name}<${method.response.$ref}>`; + if (schema && !isEmptySchema(schema)) { + return `${name}<${schemaName}>`; } else { return `${name}<{}>`; } @@ -461,34 +464,6 @@ export class App { return dir; } - /** - * Creates a callback that writes request parameters. - */ - private static createRequestParameterWriterCallback( - parameters: Record, - schemas: Record, - ref?: string - ) { - return function requestParameterWriterCallback( - writer: TypescriptTextWriter - ) { - writer.anonymousType(() => { - _.forEach(parameters, (data, key) => { - if (data.description) { - writer.comment(formatComment(data.description)); - } - - writer.property(key, getType(data, schemas), Boolean(data.required)); - }); - - if (ref) { - writer.comment('Request body'); - writer.property('resource', ref, true); - } - }); - }; - } - /** * Writes specified resource definition. */ @@ -498,38 +473,83 @@ export class App { parameters: Record = {}, schemas: Record ) { - _.forEach(resources, (resource, resourceName) => { - const resourceInterfaceName = getResourceTypeName(resourceName); + // Accumulates the request method parameter types, to be written after all of the resource interfaces. + const requestTypeWriters: (() => void)[] = []; + // Write all resource interfaces + _.forEach(resources, (resource, resourceName) => { if (resource.resources) { this.writeResources(out, resource.resources, parameters, schemas); } - out.interface(resourceInterfaceName, () => { + out.interface(getResourceTypeName(resourceName), () => { + if (resource.resources) { + _.forEach(resource.resources, (_, childResourceName) => { + const childResourceInterfaceName = getResourceTypeName( + childResourceName + ); + out.property(childResourceName, childResourceInterfaceName); + }); + } + _.forEach(resource.methods, method => { if (method.description) { out.comment(formatComment(method.description)); } - const requestRef = method.request?.$ref; - const requestParameters: Record = sortObject({ - ...parameters, - ...method.parameters, + // Construct request type, e.g. get(...) on InvitationsResource -> GetInvitationsRequest + const methodType = checkExists(getName(method.id)); + const methodParts = method.id?.split('.'); + const methodName = methodParts + ? camelCaseParts(methodParts.slice(1, methodParts.length - 1)) + : ''; + const requestTypeName = + methodType[0].toUpperCase() + + methodType.slice(1) + + methodName + + 'Request'; + + const hasResourcePolicy = !!( + parameters.resource || method.parameters?.resource + ); + + // Prepare the request object to be written + requestTypeWriters.push(() => { + out.interface(requestTypeName, (writer: TypescriptTextWriter) => { + const requestParams: Record = sortObject({ + ...parameters, + ...method.parameters, + }); + + _.forEach(requestParams, (data, key) => { + if (data.description) { + writer.comment(formatComment(data.description)); + } + writer.property( + key, + getType(data, schemas), + data.required || false + ); + }); + + // If the request takes a body (e.g. a POST request), add it as the 'resource' property. + if (method.request?.$ref && !hasResourcePolicy) { + writer.comment('Request body'); + writer.property('resource', method.request.$ref, true); + } + }); }); - if (!requestParameters.resource || !requestRef) { + const requestRef = method.request?.$ref; + if (!hasResourcePolicy || !requestRef) { // generate method(request) out.method( - formatPropertyName(checkExists(getName(method.id))), + formatPropertyName(methodType), [ { parameter: 'request', - type: App.createRequestParameterWriterCallback( - requestParameters, - schemas, - requestRef - ), - required: Boolean(requestRef), + type: requestTypeName, + required: !!method.parameters, }, ], getMethodReturn(method, schemas) @@ -539,19 +559,18 @@ export class App { if (requestRef) { // generate method(request, body) out.method( - formatPropertyName(checkExists(getName(method.id))), + formatPropertyName(methodType), [ { parameter: 'request', - type: App.createRequestParameterWriterCallback( - requestParameters, - schemas - ), - required: true, + type: `Omit<${requestTypeName}, 'resource'>`, + required: !!method.parameters, }, { parameter: 'body', - type: requestRef, + type: hasResourcePolicy + ? requestRef + : `${requestTypeName}['resource']`, required: true, }, ], @@ -559,17 +578,11 @@ export class App { ); } }); - - if (resource.resources) { - _.forEach(resource.resources, (_, childResourceName) => { - const childResourceInterfaceName = getResourceTypeName( - childResourceName - ); - out.property(childResourceName, childResourceInterfaceName); - }); - } }); }); + + // Then write all request types + requestTypeWriters.forEach(write => write()); } private static getTypingsName(api: string, version: string | null) { diff --git a/src/utils.ts b/src/utils.ts index 80a02506b6..58fd833506 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,16 +11,23 @@ export function parseVersion(version: string) { return matches ? `${matches[1] || 0}.${matches[2] || 0}` : '0.0'; } +export function camelCaseParts(parts: string[]) { + return parts.map(x => `${x[0].toUpperCase()}${x.substring(1)}`).join(''); +} + +export function getTypeName(resourceName: string) { + if (resourceName.trim() === '') { + return ''; + } + resourceName = camelCaseParts(resourceName.split(/[.-]/)); + return `${resourceName[0].toUpperCase()}${resourceName.substring(1)}`; +} + /** * Returns the capitalized name of the TypeScript interface for the specified resource. */ export function getResourceTypeName(resourceName: string) { - resourceName = resourceName - .split('-') - .map(x => `${x[0].toUpperCase()}${x.substring(1)}`) - .join(''); - - return `${resourceName[0].toUpperCase()}${resourceName.substring(1)}Resource`; + return getTypeName(resourceName) + 'Resource'; } /** From 2fa3178da9d8171a6621a992cf454214eb5b993d Mon Sep 17 00:00:00 2001 From: Declan Vong Date: Mon, 11 May 2020 17:22:37 +1000 Subject: [PATCH 2/2] autoformat --- src/app.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index 19b3a78a55..3554fc6b7a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,19 +1,18 @@ -import { promisify } from 'bluebird'; +import {promisify} from 'bluebird'; import sortObject from 'deep-sort-object'; import fs from 'fs'; import lineReader from 'line-reader'; import _ from 'lodash'; -import path, { basename, join, resolve } from 'path'; +import path, {basename, join, resolve} from 'path'; import request from 'request'; -import { Template } from './template'; +import {Template} from './template'; import { camelCaseParts, ensureDirectoryExists, getResourceTypeName, - getTypeName, parseVersion, } from './utils'; -import { StreamWriter, TextWriter } from './writer'; +import {StreamWriter, TextWriter} from './writer'; type JsonSchema = gapi.client.discovery.JsonSchema; type RestResource = gapi.client.discovery.RestResource;