Skip to content

Commit

Permalink
Add support for head-as-boolean (#502)
Browse files Browse the repository at this point in the history
When enabled, all HEAD operations will return a Success field in their
response envelope.  For HTTP status >= 200 and < 300 Success will be set
to true.
For operations that don't model header responses, return a
BooleanResponse response envelope.
Update codegen for latest azcore and armcore.
Consolidated some checks into a single byValue flag.
Removed some duplicated code.
Updated autorest.testserver to latest version.
  • Loading branch information
jhendrixMSFT authored Oct 20, 2020
1 parent a76eb91 commit d766dca
Show file tree
Hide file tree
Showing 76 changed files with 713 additions and 267 deletions.
3 changes: 2 additions & 1 deletion .scripts/regeneration.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const goMappings = {
'filegroup': 'body-file.json',
//'formdatagroup': 'body-formdata.json',
'headergroup': 'header.json',
'headgroup': 'head.json',
'httpinfrastructuregroup': 'httpInfrastructure.json',
'integergroup': 'body-integer.json',
'lrogroup': 'lro.json',
Expand All @@ -52,7 +53,7 @@ const goMappings = {
for (namespace in goMappings) {
// for each swagger run the autorest command to generate code based on the swagger for the relevant namespace and output to the /generated directory
const inputFile = swaggerDir + goMappings[namespace];
generate(inputFile, 'test/autorest/' + namespace);
generate(inputFile, 'test/autorest/' + namespace, '--head-as-boolean=true');
}

const blobStorage = 'https://raw.githubusercontent.com/Azure/azure-rest-api-specs/storage-dataplane-preview/specification/storage/data-plane/Microsoft.BlobStorage/preview/2019-07-07/blob.json';
Expand Down
12 changes: 6 additions & 6 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/generator/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ function generateContent(session: Session<CodeModel>, exportClient: boolean): st
text += `// ${defaultClientOptions} creates a ${clientOptions} type initialized with default values.\n`;
text += `func ${defaultClientOptions}() ${clientOptions} {\n`;
text += `\treturn ${clientOptions}{\n`;
text += '\t\tHTTPClient: azcore.DefaultHTTPClientTransport(),\n';
text += '\t\tRetry: azcore.DefaultRetryOptions(),\n';
if (isARM && session.model.security.authenticationRequired) {
text += '\t\tRegisterRPOptions: armcore.DefaultRegistrationOptions(),\n';
Expand Down
4 changes: 2 additions & 2 deletions src/generator/gomod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export async function generateGoModFile(session: Session<CodeModel>): Promise<st
text += 'go 1.13\n\n';
// here we specify the minimum version of armcore/azcore as required by the code generator
// TODO: come up with a way to get the latest minor/patch versions.
const azcore = 'github.com/Azure/azure-sdk-for-go/sdk/azcore v0.12.0';
const azcore = 'github.com/Azure/azure-sdk-for-go/sdk/azcore v0.13.0';
if (session.model.language.go!.openApiType === 'arm') {
text += 'require (\n';
text += '\tgithub.com/Azure/azure-sdk-for-go/sdk/armcore v0.3.3\n';
text += '\tgithub.com/Azure/azure-sdk-for-go/sdk/armcore v0.3.4\n';
text += `\t${azcore}\n`;
text += ')\n'
} else {
Expand Down
6 changes: 2 additions & 4 deletions src/generator/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,7 @@ class StructDef {
tag = '';
}
let pointer = '*';
if (prop.schema.language.go!.discriminatorInterface || prop.schema.language.go!.lroPointerException) {
// pointer-to-interface introduces very clunky code
if (prop.schema.language.go!.byValue) {
pointer = '';
}
text += `\t${prop.language.go!.name} ${pointer}${typeName}${tag}\n`;
Expand All @@ -187,8 +186,7 @@ class StructDef {
text += `\t${comment(param.language.go!.description, '// ')}\n`;
}
let pointer = '*';
if (param.required || param.schema.language.go!.discriminatorInterface) {
// pointer-to-interface introduces very clunky code
if (param.required || param.schema.language.go!.byValue) {
pointer = '';
}
text += `\t${pascalCase(param.language.go!.name)} ${pointer}${param.schema.language.go!.name}\n`;
Expand Down
55 changes: 38 additions & 17 deletions src/generator/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,20 +264,32 @@ function generateOperation(op: Operation, imports: ImportManager): string {
text += `\tif err != nil {\n`;
text += `\t\treturn nil, err\n`;
text += `\t}\n`;
text += `\tif !resp.HasStatusCode(${formatStatusCodes(statusCodes)}) {\n`;
text += `\t\treturn nil, client.${info.protocolNaming.errorMethod}(resp)\n`;
text += '\t}\n';
if (isLROOperation(op)) {
text += '\t return resp, nil\n';
} else if (needsResponseHandler(op)) {
// also cheating here as at present the only param to the responder is an azcore.Response
text += `\tresult, err := client.${info.protocolNaming.responseMethod}(resp)\n`;
text += `\tif err != nil {\n`;
text += `\t\treturn nil, err\n`;
text += `\t}\n`;
text += `\treturn result, nil\n`;
// HAB with schema response is handled in protocol responder
if (op.language.go!.headAsBoolean && !(op.responses && isSchemaResponse(op.responses[0]))) {
let respEnv = 'BooleanResponse';
text += '\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n';
text += `\t\treturn &${respEnv}{RawResponse: resp.Response, Success: true}, nil\n`;
text += '\t} else if resp.StatusCode >= 400 && resp.StatusCode < 500 {\n';
text += `\t\treturn &${respEnv}{RawResponse: resp.Response, Success: false}, nil\n`;
text += '\t} else {\n';
text += `\t\treturn nil, client.${info.protocolNaming.errorMethod}(resp)\n`;
text += '\t}\n';
} else {
text += '\treturn resp.Response, nil\n';
text += `\tif !resp.HasStatusCode(${formatStatusCodes(statusCodes)}) {\n`;
text += `\t\treturn nil, client.${info.protocolNaming.errorMethod}(resp)\n`;
text += '\t}\n';
if (isLROOperation(op)) {
text += '\t return resp, nil\n';
} else if (needsResponseHandler(op)) {
// also cheating here as at present the only param to the responder is an azcore.Response
text += `\tresult, err := client.${info.protocolNaming.responseMethod}(resp)\n`;
text += `\tif err != nil {\n`;
text += `\t\treturn nil, err\n`;
text += `\t}\n`;
text += `\treturn result, nil\n`;
} else {
text += '\treturn resp.Response, nil\n';
}
}
text += '}\n\n';
return text;
Expand Down Expand Up @@ -570,7 +582,7 @@ function needsResponseHandler(op: Operation): boolean {
return hasSchemaResponse(op) || (isLROOperation(op) && hasSchemaResponse(op)) || isPageableOperation(op);
}

function generateResponseUnmarshaller(response: Response, imports: ImportManager): string {
function generateResponseUnmarshaller(op: Operation, response: Response, imports: ImportManager): string {
let unmarshallerText = '';
if (!isSchemaResponse(response)) {
throw console.error('TODO');
Expand Down Expand Up @@ -610,6 +622,11 @@ function generateResponseUnmarshaller(response: Response, imports: ImportManager
const schemaResponse = <SchemaResponse>response;
let respObj = `${schemaResponse.schema.language.go!.responseType.name}{RawResponse: resp.Response}`;
unmarshallerText += `\tresult := ${respObj}\n`;
if (op.language.go!.headAsBoolean) {
unmarshallerText += '\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n';
unmarshallerText += '\t\tresult.Success = true\n';
unmarshallerText += '\t}\n';
}
// assign any header values
for (const prop of values(<Array<Property>>schemaResponse.schema.language.go!.properties)) {
if (prop.language.go!.fromHeader) {
Expand Down Expand Up @@ -641,13 +658,13 @@ function createProtocolResponse(op: Operation, imports: ImportManager): string {
let text = `${comment(name, '// ')} handles the ${info.name} response.\n`;
text += `func (client *${clientName}) ${name}(resp *azcore.Response) (${generateReturnsInfo(op, 'handler').join(', ')}) {\n`;
if (!isMultiRespOperation(op)) {
text += generateResponseUnmarshaller(op.responses![0], imports);
text += generateResponseUnmarshaller(op, op.responses![0], imports);
} else {
imports.add('fmt');
text += '\tswitch resp.StatusCode {\n';
for (const response of values(op.responses)) {
text += `\tcase ${formatStatusCodes(response.protocol.http!.statusCodes)}:\n`
text += generateResponseUnmarshaller(response, imports);
text += generateResponseUnmarshaller(op, response, imports);
}
text += '\tdefault:\n';
text += `\t\treturn nil, fmt.Errorf("unhandled HTTP status code %d", resp.StatusCode)\n`;
Expand Down Expand Up @@ -883,6 +900,10 @@ function generateReturnsInfo(op: Operation, apiType: 'int' | 'op' | 'handler'):
} else if (hasSchemaResponse(op)) {
// simple schema response
returnType = '*' + (<SchemaResponse>op.responses![0]).schema.language.go!.responseType.name;
} else if (op.language.go!.headAsBoolean) {
// NOTE: this case must come after the hasSchemaResponse() check to properly handle
// the intersection of head-as-boolean with modeled response headers
return ['*BooleanResponse', 'error'];
}
return [returnType, 'error'];
}
Expand Down Expand Up @@ -934,7 +955,7 @@ function generateARMLROBeginMethod(op: Operation, imports: ImportManager): strin
text += `\t\t\treturn client.${info.protocolNaming.errorMethod}(resp)\n`;
text += '\t\t},\n';
text += `\t\trespHandler: func(resp *azcore.Response) (*${(<SchemaResponse>op.responses![0]).schema.language.go!.responseType.name}, error) {\n`;
text += generateResponseUnmarshaller(op.responses![0], imports);
text += generateResponseUnmarshaller(op, op.responses![0], imports);
text += '\t\t},\n';
text += `\t\tstatusCodes: []int{${formatStatusCodes(statusCodes)}},\n`;
}
Expand Down
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"typescript": "~3.7.2",
"@typescript-eslint/eslint-plugin": "~2.6.0",
"@typescript-eslint/parser": "~2.6.0",
"@microsoft.azure/autorest.testserver": "2.10.58",
"@microsoft.azure/autorest.testserver": "2.10.60",
"@autorest/autorest": "~3.0.6173",
"eslint": "~6.6.0",
"@azure-tools/codegen": "~2.5.288",
Expand Down
1 change: 1 addition & 0 deletions src/transform/namer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export async function namer(session: Session<CodeModel>) {
if (obj.discriminator) {
// if this is a discriminator add the interface name
details.discriminatorInterface = createPolymorphicInterfaceName(details.name);
details.byValue = true;
details.discriminatorTypes = new Array<string>();
details.discriminatorTypes.push('*' + details.name);
for (const child of values(obj.discriminator.all)) {
Expand Down
57 changes: 51 additions & 6 deletions src/transform/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export async function transform(host: Host) {

async function process(session: Session<CodeModel>) {
const specType = await session.getValue('openapi-type', 'not_specified');
const headAsBoolean = await session.getValue('head-as-boolean', false);
session.model.language.go!.openApiType = specType;
session.model.language.go!.headAsBoolean = headAsBoolean;
processOperationRequests(session);
processOperationResponses(session);
// fix up dictionary element types (additional properties)
Expand Down Expand Up @@ -556,7 +558,42 @@ function createResponseEnvelope(codeModel: CodeModel, group: OperationGroup, op:
}
}
}
const createRawResponseProp = function (): Property {
return newProperty('RawResponse', 'RawResponse contains the underlying HTTP response.', newObject('http.Response', 'raw HTTP response'));
}
const createSuccessProp = function (): Property {
const successProp = newObject('bool', 'bool response');
successProp.language.go!.byValue = true;
return newProperty('Success', 'Success indicates if the operation succeeded or failed.', successProp);
}
// if head-as-boolean (HAB) is enabled and this is a HEAD method then return a BooleanResponse
// response envelope instead of http.Response. only do this if the operation doesn't model any
// header values (HAB will be enabled for that response envelope).
if (codeModel.language.go!.headAsBoolean && op.requests![0].protocol.http!.method === 'head' && headers.size === 0) {
// enable treating HEAD requests as boolean responses.
op.language.go!.headAsBoolean = codeModel.language.go!.headAsBoolean;
const name = 'BooleanResponse';
if (!responseEnvelopeExists(codeModel, name)) {
const description = `${name} contains a boolean response.`;
const respEnv = newObject(name, description);

respEnv.language.go!.properties = [
createRawResponseProp(),
createSuccessProp(),
];
// mark as a response type
respEnv.language.go!.responseType = {
name: name,
description: description,
responseType: true,
}
// add this response schema to the global list of response
const responseEnvelopes = <Array<Schema>>codeModel.language.go!.responseEnvelopes;
responseEnvelopes.push(respEnv);
}
// HEAD operations will never be pagable, LROs etc so exit
return;
}
// if the response defines a schema then add it as a field to the response type.
// only do this if the response schema hasn't been processed yet.
for (const response of values(op.responses)) {
Expand All @@ -568,13 +605,18 @@ function createResponseEnvelope(codeModel: CodeModel, group: OperationGroup, op:
const description = `${name} contains the response from method ${group.language.go!.name}.${op.language.go!.name}.`;
const respEnv = newObject(name, description);
respEnv.language.go!.properties = [
newProperty('RawResponse', 'RawResponse contains the underlying HTTP response.', newObject('http.Response', 'raw HTTP response'))
createRawResponseProp()
];
for (const item of items(headers)) {
const prop = newProperty(item.key, item.value.description, item.value.schema);
prop.language.go!.fromHeader = item.value.header;
(<Array<Property>>respEnv.language.go!.properties).push(prop);
}
if (codeModel.language.go!.headAsBoolean && op.requests![0].protocol.http!.method === 'head') {
// this is the intersection of head-as-boolean with modeled header responses
op.language.go!.headAsBoolean = codeModel.language.go!.headAsBoolean;
(<Array<Property>>respEnv.language.go!.properties).push(createSuccessProp());
}
// mark as a response type
respEnv.language.go!.responseType = {
name: name,
Expand All @@ -592,7 +634,7 @@ function createResponseEnvelope(codeModel: CodeModel, group: OperationGroup, op:
} else if (!responseEnvelopeCreated(codeModel, response.schema)) {
response.schema.language.go!.responseType = generateResponseEnvelopeName(response.schema);
response.schema.language.go!.properties = [
newProperty('RawResponse', 'RawResponse contains the underlying HTTP response.', newObject('http.Response', 'HTTP response'))
createRawResponseProp()
];
const marshallingFormat = getMarshallingFormat(response.protocol);
response.schema.language.go!.responseType.marshallingFormat = marshallingFormat;
Expand Down Expand Up @@ -930,12 +972,15 @@ function generateLROResponseEnvelope(response: Response, op: Operation, codeMode
response.schema.language.go!.lroResponseType = respTypeObject;
}
// create PollUntilDone
const pollerFunc = newObject(`func(ctx context.Context, frequency time.Duration) (${pollerResponse}, error)`, 'PollUntilDone');
pollerFunc.language.go!.byValue = true;
const pollUntilDone = newProperty('PollUntilDone', 'PollUntilDone will poll the service endpoint until a terminal state is reached or an error is received',
newObject(`func(ctx context.Context, frequency time.Duration) (${pollerResponse}, error)`, 'PollUntilDone'));
pollUntilDone.schema.language.go!.lroPointerException = true;
pollerFunc);
pollUntilDone.language.go!.byValue = true;
// create Poller
const poller = newProperty('Poller', 'Poller contains an initialized poller.', newObject(pollerTypeName, 'poller'));
poller.schema.language.go!.lroPointerException = true;
const pollerType = newObject(pollerTypeName, 'poller');
pollerType.language.go!.byValue = true;
const poller = newProperty('Poller', 'Poller contains an initialized poller.', pollerType);
respTypeObject.language.go!.properties = [
newProperty('RawResponse', 'RawResponse contains the underlying HTTP response.', newObject('http.Response', 'HTTP response')),
pollUntilDone,
Expand Down
5 changes: 2 additions & 3 deletions test/autorest/additionalpropsgroup/zz_generated_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions test/autorest/arraygroup/zz_generated_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions test/autorest/azurereportgroup/zz_generated_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions test/autorest/azurespecialsgroup/zz_generated_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions test/autorest/azurespecialsgroup/zz_generated_header.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d766dca

Please sign in to comment.