Skip to content

Commit

Permalink
Separate poller response envelopes (#429)
Browse files Browse the repository at this point in the history
* Adding poller response envelope transformations and generation

* Updating LRO response type creation to one place in loop

* skipping lro+pagers

* consolidate LRO response envelope creation

* ensure unique pollers for scalar APIs

* improve comment on LRO response envelopes

Co-authored-by: Joel Hendrix <[email protected]>
Co-authored-by: Catalina Peralta <[email protected]>
  • Loading branch information
3 people authored Jun 24, 2020
1 parent 6336f43 commit 7a846e5
Show file tree
Hide file tree
Showing 12 changed files with 496 additions and 491 deletions.
3 changes: 1 addition & 2 deletions src/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,12 @@ export function isPageableOperation(op: Operation): boolean {

export interface PollerInfo {
name: string;
responseType: string;
op: Operation;
}

// returns true if the operation is a long-running operation
export function isLROOperation(op: Operation): boolean {
return op.extensions?.['x-ms-long-running-operation'];
return op.extensions?.['x-ms-long-running-operation'] === true;
}

// returns ObjectSchema type predicate if the schema is an ObjectSchema
Expand Down
27 changes: 19 additions & 8 deletions src/generator/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ function generateOperation(clientName: string, op: Operation, imports: ImportMan
if (isLROOperation(op)) {
// TODO remove LRO for pageable responses NYI
if (op.extensions!['x-ms-pageable']) {
text += `\treturn nil, nil`;
text += `\treturn nil, nil\n`;
text += '}\n\n';
return text;
}
Expand Down Expand Up @@ -655,9 +655,10 @@ function createProtocolResponse(client: string, op: Operation, imports: ImportMa
}
const generateResponseUnmarshaller = function (response: Response): string {
let unmarshallerText = '';
const isLRO = isLROOperation(op);
if (!isSchemaResponse(response)) {
if (isLROOperation(op)) {
unmarshallerText += '\treturn &HTTPResponse{RawResponse: resp.Response}, nil\n';
if (isLRO) {
unmarshallerText += '\treturn &HTTPPollerResponse{RawResponse: resp.Response}, nil\n';
return unmarshallerText;
}
// no response body, return the *http.Response
Expand Down Expand Up @@ -697,6 +698,11 @@ function createProtocolResponse(client: string, op: Operation, imports: ImportMa
return unmarshallerText;
}
const schemaResponse = <SchemaResponse>response;
// TODO remove paging skip when adding lro + pagers
if (isLRO && !isPageableOperation(op)) {
unmarshallerText += `\treturn &${schemaResponse.schema.language.go!.lroResponseType.language.go!.name}{RawResponse: resp.Response}, nil\n`;
return unmarshallerText;
}
let respObj = `${schemaResponse.schema.language.go!.responseType.name}{RawResponse: resp.Response}`;
unmarshallerText += `\tresult := ${respObj}\n`;
// assign any header values
Expand Down Expand Up @@ -768,7 +774,6 @@ function createProtocolErrHandler(client: string, op: Operation, imports: Import
}
return errors.New(string(body))
`;

}

// if the response doesn't define any error types return a generic error
Expand Down Expand Up @@ -1039,14 +1044,20 @@ function generateReturnsInfo(op: Operation, forHandler: boolean): string[] {
if (isMultiRespOperation(op)) {
returnType = 'interface{}';
} else {
const firstResp = op.responses![0];
// must check pageable first as all pageable operations are also schema responses
if (!forHandler && isPageableOperation(op)) {
const firstResp = <SchemaResponse>op.responses![0];
// must check pageable first as all pageable operations are also schema responses,
// but LRO operations that return a pager are an exception and need to return LRO specific
// responses
if (!forHandler && isPageableOperation(op) && !isLROOperation(op)) {
returnType = op.language.go!.pageableType.name;
} else if (isSchemaResponse(firstResp)) {
returnType = '*' + firstResp.schema.language.go!.responseType.name;
// TODO remove paging skip when adding LRO + pagers
if (isLROOperation(op) && !isPageableOperation(op)) {
returnType = '*' + firstResp.schema.language.go!.lroResponseType.language.go!.name;
}
} else if (isLROOperation(op)) {
returnType = '*HTTPResponse';
returnType = '*HTTPPollerResponse';
}
}
return [returnType, 'error'];
Expand Down
4 changes: 2 additions & 2 deletions src/generator/pollers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export async function generatePollers(session: Session<CodeModel>): Promise<stri
let handleResponse = '';
const schemaResponse = <SchemaResponse>poller.op.responses![0];
let unmarshalResponse = 'nil';
if (isSchemaResponse(schemaResponse) && schemaResponse.schema.language.go!.responseType.value != undefined) {
if (isSchemaResponse(schemaResponse) && schemaResponse.schema.language.go!.responseType.name !== undefined) {
responseType = schemaResponse.schema.language.go!.responseType.name;
pollUntilDoneResponse = `(*${responseType}, error)`;
pollUntilDoneReturn = 'p.FinalResponse(ctx)';
Expand Down Expand Up @@ -117,7 +117,7 @@ export async function generatePollers(session: Session<CodeModel>): Promise<stri
handleResponse = `
func (p *${pollerName}) handleResponse(resp *azcore.Response) (*${responseType}, error) {
result := ${responseType}{RawResponse: resp.Response}
if (resp.HasStatusCode(http.StatusNoContent)) {
if resp.HasStatusCode(http.StatusNoContent) {
return &result, nil
}
if !resp.HasStatusCode(pollingCodes[:]...) {
Expand Down
158 changes: 98 additions & 60 deletions src/transform/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,9 @@ interface HttpHeaderWithDescription extends HttpHeader {
description: string;
}

// the name of the struct field for scalar responses (int, string, etc)
const scalarResponsePropName = 'Value';

// creates the response type to be returned from an operation and updates the operation
function createResponseType(codeModel: CodeModel, group: OperationGroup, op: Operation) {
// create the `type <type>Response struct` response
Expand Down Expand Up @@ -488,35 +491,8 @@ function createResponseType(codeModel: CodeModel, group: OperationGroup, op: Ope
for (const response of values(op.responses)) {
if (!isSchemaResponse(response)) {
// the response doesn't return a model. if it returns
// headers then create a model that contains them.
if (isLROOperation(op)) {
const name = 'HTTPResponse';
const description = `${name} contains the HTTP response from the call to the service endpoint`;
const object = new ObjectSchema(name, description);
object.language.go = object.language.default;
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) (*http.Response, error)', 'TODO'));
const getPoller = newProperty('Poller', 'Poller contains an initialized poller', newObject('HTTPPoller', 'TODO'));
pollUntilDone.schema.language.go!.lroPointerException = true;
getPoller.schema.language.go!.lroPointerException = true;
object.language.go!.properties = [
newProperty('RawResponse', 'RawResponse contains the underlying HTTP response.', newObject('http.Response', 'raw HTTP response')),
pollUntilDone,
getPoller
];
// mark as a response type
object.language.go!.responseType = {
name: name,
description: description,
responseType: true,
};
if (!responseExists(codeModel, object.language.go!.responseType.name)) {
// add this response schema to the global list of response
const responseSchemas = <Array<Schema>>codeModel.language.go!.responseSchemas;
responseSchemas.push(object);
// attach it to the response
(<SchemaResponse>response).schema = object;
}
} else if (headers.size > 0) {
// headers then create a model that contains them, except for LROs.
if (headers.size > 0 && !isLROOperation(op)) {
const name = `${group.language.go!.name}${op.language.go!.name}Response`;
const description = `${name} contains the response from method ${group.language.go!.name}.${op.language.go!.name}.`;
const object = new ObjectSchema(name, description);
Expand All @@ -543,15 +519,15 @@ function createResponseType(codeModel: CodeModel, group: OperationGroup, op: Ope
(<SchemaResponse>response).schema = object;
}
}
} else if (!responseTypeCreated(codeModel, response.schema) || isLROOperation(op)) {
} else if (!responseTypeCreated(codeModel, response.schema)) {
response.schema.language.go!.responseType = generateResponseTypeName(response.schema);
response.schema.language.go!.properties = [
newProperty('RawResponse', 'RawResponse contains the underlying HTTP response.', newObject('http.Response', 'TODO'))
];
const marshallingFormat = getMarshallingFormat(response.protocol);
response.schema.language.go!.responseType.marshallingFormat = marshallingFormat;
// for operations that return scalar types we use a fixed field name 'Value'
let propName = 'Value';
// for operations that return scalar types we use a fixed field name
let propName = scalarResponsePropName;
if (response.schema.type === SchemaType.Object) {
// for object types use the type's name as the field name
propName = response.schema.language.go!.name;
Expand All @@ -564,37 +540,28 @@ function createResponseType(codeModel: CodeModel, group: OperationGroup, op: Ope
propName = pascalCase(response.schema.serialization.xml.name);
}
response.schema.language.go!.responseType.value = propName;
(<Array<Property>>response.schema.language.go!.properties).push(newProperty(propName, response.schema.language.go!.description, response.schema));
// add any headers to the response type
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>>response.schema.language.go!.properties).push(prop);
}
if (isLROOperation(op)) {
response.schema.language.go!.isLRO = true;
let prop = 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) (*${response.schema.language.go!.responseType.name}, error)`, 'TODO'));
prop.schema.language.go!.lroPointerException = true;
(<Array<Property>>response.schema.language.go!.properties).push(prop);
prop = newProperty('Poller', 'Poller contains an initialized poller', newObject(`${response.schema.language.go!.responseType.value}Poller`, 'TODO'));
prop.schema.language.go!.lroPointerException = true;
(<Array<Property>>response.schema.language.go!.properties).push(prop);
// for LROs add a specific poller response envelope to return from Begin operations
if (!isLROOperation(op)) {
// exclude LRO headers from Widget response envelopes
// add any headers to the response type
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>>response.schema.language.go!.properties).push(prop);
}
}
// the Widget response doesn't belong in the poller response envelope
(<Array<Property>>response.schema.language.go!.properties).push(newProperty(propName, response.schema.language.go!.description, response.schema));
if (!responseExists(codeModel, response.schema.language.go!.name)) {
// add this response schema to the global list of response
const responseSchemas = <Array<Schema>>codeModel.language.go!.responseSchemas;
responseSchemas.push(response.schema);
} else if (isLROOperation(op)) {
// add this response schema with LRO fields to the global list of responses by
// replacing the previously added response with the same name
const responseSchemas = <Array<Schema>>codeModel.language.go!.responseSchemas;
for (let i = 0; i < responseSchemas.length; i++) {
if (responseSchemas[i].language.go!.name === response.schema.language.go!.name) {
responseSchemas.splice(i, 1, response.schema);
}
}
}
}
// TODO: remove skipping this for pageable operations when adding lro+pager work
if (isLROOperation(op) && !op.extensions!['x-ms-pageable'] && !isPageableOperation(op)) {
generateLROResponseType(response, op, codeModel);
}
// create pageable type info
if (isPageableOperation(op)) {
if (codeModel.language.go!.pageableTypes === undefined) {
Expand Down Expand Up @@ -629,11 +596,10 @@ function createResponseType(codeModel: CodeModel, group: OperationGroup, op: Ope
}
// Determine the type of poller that needs to be added based on whether a schema is specified in the response
// if there is no schema specified for the operation response then a simple HTTP poller will be instantiated
let type = 'HTTP';
let name = 'HTTPPoller';
if (isSchemaResponse(response) && response.schema.language.go!.responseType.value) {
type = response.schema.language.go!.responseType.value;
name = generateLROPollerName(response);
}
const name = `${type}Poller`;
const pollers = <Array<PollerInfo>>codeModel.language.go!.pollerTypes;
let skipAddLRO = false;
for (const poller of values(pollers)) {
Expand All @@ -649,7 +615,6 @@ function createResponseType(codeModel: CodeModel, group: OperationGroup, op: Ope
// create a new one, add to global list and assign to method
const poller = {
name: name,
responseType: type,
op: op,
};
pollers.push(poller);
Expand Down Expand Up @@ -773,7 +738,80 @@ function generateResponseTypeName(schema: Schema): Language {
name: name,
description: `${name} is the response envelope for operations that return a ${schema.language.go!.name} type.`,
responseType: true,
};
}

// generate LRO response type name is separate from the general response type name
// generation, since it requires returning the poller response envelope
function generateLROResponseTypeName(response: Response): Language {
// default to generic response envelope
let name = 'HTTPPollerResponse'
let desc = `${name} contains the asynchronous HTTP response from the call to the service endpoint.`;
if (isSchemaResponse(response)) {
// create a type-specific response envelope
const typeName = recursiveTypeName(response.schema) + 'Poller';
name = `${typeName}Response`;
desc = `${name} is the response envelope for operations that asynchronously return a ${response.schema.language.go!.name} type.`;
}
return {
name: name,
description: desc,
responseType: true,
};
}

function generateLROPollerName(schemaResp: SchemaResponse): string {
if (schemaResp.schema.language.go!.responseType.value === scalarResponsePropName) {
// for scalar responses, use the underlying type name for the poller
return `${pascalCase(schemaResp.schema.language.go!.name)}Poller`;
}
return `${schemaResp.schema.language.go!.responseType.value}Poller`;
}

function generateLROResponseType(response: Response, op: Operation, codeModel: CodeModel) {
const respTypeName = generateLROResponseTypeName(response);
if (responseExists(codeModel, respTypeName.name)) {
return;
}
const respTypeObject = newObject(respTypeName.name, respTypeName.description);
respTypeObject.language.go!.responseType = respTypeName;
let pollerResponse: string;
let pollerTypeName: string;
if (!isSchemaResponse(response)) {
pollerResponse = '*http.Response';
pollerTypeName = 'HTTPPoller';
// mark as a response type
respTypeObject.language.go!.responseType = {
name: respTypeName.name,
description: respTypeName.description,
responseType: true,
};
} else if (isPageableOperation(op)) {
pollerResponse = `${(<SchemaResponse>response).schema.language.go!.name}Pager`;
pollerTypeName = `${(<SchemaResponse>response).schema.language.go!.name}PagerPoller`;
response.schema.language.go!.isLRO = true;
response.schema.language.go!.lroResponseType = respTypeObject;
} else {
pollerResponse = `*${response.schema.language.go!.responseType.name}`;
pollerTypeName = generateLROPollerName(response);
response.schema.language.go!.isLRO = true;
response.schema.language.go!.lroResponseType = respTypeObject;
}
// create PollUntilDone
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)`, 'TODO'));
pollUntilDone.schema.language.go!.lroPointerException = true;
// create Poller
const poller = newProperty('Poller', 'Poller contains an initialized poller.', newObject(pollerTypeName, 'TODO'));
poller.schema.language.go!.lroPointerException = true;
respTypeObject.language.go!.properties = [
newProperty('RawResponse', 'RawResponse contains the underlying HTTP response.', newObject('http.Response', 'TODO')),
pollUntilDone,
poller
];
// add the LRO response schema to the global list of response
const responseSchemas = <Array<Schema>>codeModel.language.go!.responseSchemas;
responseSchemas.push(respTypeObject);
}

function getRootDiscriminator(obj: ObjectSchema): ObjectSchema {
Expand Down
Loading

0 comments on commit 7a846e5

Please sign in to comment.