powershell/llcsharp/operation/method.ts (707 lines of code) (raw):

/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { NewResponse, ParameterLocation } from '@azure-tools/codemodel-v3'; import { Operation, SchemaResponse, BinaryResponse, Schema as NewSchema, Response, BinarySchema } from '@autorest/codemodel'; import { items, values, keys, Dictionary, length } from '@azure-tools/linq'; import { EOL, DeepPartial } from '@azure-tools/codegen'; import { Access, Else, IsNull, Modifier, Or, TypeDeclaration } from '@azure-tools/codegen-csharp'; import { Class } from '@azure-tools/codegen-csharp'; import { Binary } from '../schema/binary'; import { Expression, ExpressionOrLiteral, LiteralExpression, StringExpression, toExpression, valueOf } from '@azure-tools/codegen-csharp'; import { Method } from '@azure-tools/codegen-csharp'; import { Parameter } from '@azure-tools/codegen-csharp'; import { Case, DefaultCase, TerminalCase, TerminalDefaultCase } from '@azure-tools/codegen-csharp'; import { Finally } from '@azure-tools/codegen-csharp'; import { If, While } from '@azure-tools/codegen-csharp'; import { Return } from '@azure-tools/codegen-csharp'; import { OneOrMoreStatements, Statement, Statements } from '@azure-tools/codegen-csharp'; import { Switch } from '@azure-tools/codegen-csharp'; import { Try } from '@azure-tools/codegen-csharp'; import { Using } from '@azure-tools/codegen-csharp'; import { Local, LocalVariable, Variable } from '@azure-tools/codegen-csharp'; import { ClientRuntime } from '../clientruntime'; import { HttpOperation, Schema } from '../code-model'; import { State } from '../generator'; import { CallbackParameter, OperationParameter, OperationBodyParameter } from '../operation/parameter'; import { getAllProperties as NewGetAllProperties, getAllPublicVirtualProperties as NewGetAllPublicVirtualProperties, getVirtualPropertyFromPropertyName as NewGetVirtualPropertyFromPropertyName, VirtualProperty as NewVirtualProperty } from '../../utils/schema'; import { isMediaTypeJson, isMediaTypeXml, KnownMediaType, knownMediaType, normalizeMediaType, parseMediaType } from '@azure-tools/codemodel-v3'; import { ClassType, dotnet, System } from '@azure-tools/codegen-csharp'; import { Ternery } from '@azure-tools/codegen-csharp'; import { EnhancedTypeDeclaration } from '../schema/extended-type-declaration'; function removeEncoding(pp: OperationParameter, paramName: string, kmt: KnownMediaType): string { const up = pp.typeDeclaration.serializeToNode(kmt, pp, paramName, ClientRuntime.SerializationMode.None).value; return pp.param.extensions && pp.param.extensions['x-ms-skip-url-encoding'] ? up.replace(/global::System.Uri.EscapeDataString|System.Uri.EscapeDataString/g, '') : up; } export class EventListener { constructor(protected expression: Expression, protected emitSignals: boolean, withResult?: boolean) { this.withResult = withResult; } private withResult: boolean | undefined; *signalNoCheck(eventName: Expression, ...additionalParameters: Array<string | Expression>) { if (this.emitSignals) { const params = length(additionalParameters) > 0 ? `, ${additionalParameters.joinWith(each => typeof each === 'string' ? each : each.value)}` : ''; yield `await ${this.expression.value}.Signal(${eventName}${params});`; } } *syncSignalNoCheck(eventName: Expression, ...additionalParameters: Array<string | Expression>) { if (this.emitSignals) { const params = length(additionalParameters) > 0 ? `, ${additionalParameters.joinWith(each => typeof each === 'string' ? each : each.value)}` : ''; yield `${this.expression.value}.Signal(${eventName}${params}).Wait();`; } } *signal(eventName: Expression, ...additionalParameters: Array<string | Expression>) { if (this.emitSignals) { const params = length(additionalParameters) > 0 ? `, ${additionalParameters.joinWith(each => typeof each === 'string' ? each : each.value)}` : ''; yield `await ${this.expression.value}.Signal(${eventName}${params}); if( ${this.expression.value}.Token.IsCancellationRequested ) { return${this.withResult ? ' null' : ''}; }`; } else { yield `if( ${this.expression.value}.CancellationToken.IsCancellationRequested ) { throw ${System.OperationCanceledException.new()}; }`; } } *syncSignal(eventName: Expression, ...additionalParameters: Array<string | Expression>) { if (this.emitSignals) { const params = length(additionalParameters) > 0 ? `, ${additionalParameters.joinWith(each => typeof each === 'string' ? each : each.value)}` : ''; yield `${this.expression.value}.Signal(${eventName}${params}).Wait(); if( ${this.expression.value}.Token.IsCancellationRequested ) { return${this.withResult ? ' null' : ''}; }`; } else { yield `if( ${this.expression.value}.CancellationToken.IsCancellationRequested ) { throw ${System.OperationCanceledException.new()} }`; } } } export class OperationMethod extends Method { public methodParameters: Array<OperationParameter>; public bodyParameter?: OperationBodyParameter; public serializationMode?: LiteralExpression; public contextParameter!: Parameter; public senderParameter!: Parameter; public serializationModeParameter!: Parameter; public resourceUri!: Parameter; public callbacks = new Array<CallbackParameter>(); protected callName: string; constructor(public parent: Class, public operation: Operation, public viaIdentity: boolean, protected state: State, public viaJson: boolean = false, withResult?: boolean, objectInitializer?: DeepPartial<OperationMethod>) { super( viaJson ? `${operation.language.csharp?.name}ViaJsonString${withResult ? 'WithResult' : ''}` : viaIdentity ? `${operation.language.csharp?.name}ViaIdentity${withResult ? 'WithResult' : ''}` : `${operation.language.csharp?.name}${withResult ? 'WithResult' : ''}` || '', withResult ? System.Threading.Tasks.Task(ResolveResponseType(undefined, operation, state)) : System.Threading.Tasks.Task() ); this.apply(objectInitializer); this.async = Modifier.Async; this.returnsDescription = `A <see cref="${withResult ? System.Threading.Tasks.Task(ResolveResponseType(undefined, operation, state)) : System.Threading.Tasks.Task()}" /> that will be complete when handling of the response is completed.`; const $this = this; this.callName = `${operation.language.csharp?.name}${withResult ? 'WithResult' : ''}_Call`; this.push(Using('NoSynchronizationContext', '')); // add parameters this.methodParameters = []; const identity = new Parameter('viaIdentity', System.String); if (this.viaIdentity) { this.addParameter(identity); } let baseUrl = ''; const paths = []; const headers = []; const queries = []; const others = []; for (let index = 0; index < length(this.operation.parameters) && this.operation.parameters; index++) { const value = this.operation.parameters[index]; if (value.language.default.name === '$host') { baseUrl = value.clientDefaultValue; continue; } const p = new OperationParameter(this, value, this.state.path('parameters', index)); if (value.language.csharp?.constantValue) { const constTd = state.project.modelsNamespace.NewResolveTypeDeclaration(value.schema, true, state); p.defaultInitializer = constTd.deserializeFromString(KnownMediaType.UriParameter, new StringExpression(`${value.language.csharp.constantValue}`), toExpression(constTd.defaultOfType)); } // don't add path parameters when we're in identity mode if (!this.viaIdentity || value.protocol.http?.in !== ParameterLocation.Path) { switch (value.protocol.http?.in) { case ParameterLocation.Path: paths.push(p); break; case ParameterLocation.Header: headers.push(p); break; case ParameterLocation.Query: queries.push(p); break; default: others.push(p); break; } } else { this.add(function* () { yield ''; }); } this.methodParameters.push(p); } [...paths, ...headers, ...queries, ...others].forEach(p => this.addParameter(p)); if (baseUrl === '') { // Some services will make the host as an input parameter baseUrl = this.operation.requests ? this.operation.requests[0].protocol.http?.uri : ''; } this.description = this.operation.language.csharp?.description || ''; // add body paramter if there should be one. if (this.operation.requests && this.operation.requests.length && this.operation.requests[0].parameters && this.operation.requests[0].parameters.length) { // this request does have a request body. const param = this.operation.requests[0].parameters.find((p) => !p.origin || p.origin.indexOf('modelerfour:synthesized') < 0); if (param) { if (!viaJson) { this.bodyParameter = new OperationBodyParameter(this, 'body', param.language.default.description, param.schema, param.required ?? false, this.state, { // TODO: temp solution. We need a class like NewKnowMediaType mediaType: knownMediaType(KnownMediaType.Json), contentType: KnownMediaType.Json }); this.addParameter(this.bodyParameter); } else { this.addParameter( new Parameter('jsonString', System.String, { description: `Json string supplied to the ${operation.language.csharp?.name} operation`, }) ); } } } for (const response of [...values(this.operation.responses), ...values(this.operation.exceptions)]) { const responseType = (<BinaryResponse>response).binary ? new Binary(new BinarySchema(''), true) : ((<SchemaResponse>response).schema ? state.project.modelsNamespace.NewResolveTypeDeclaration(<NewSchema>((<SchemaResponse>response).schema), true, state) : null); const headerType = response.language.default.headerSchema ? state.project.modelsNamespace.NewResolveTypeDeclaration(<NewSchema>response.language.default.headerSchema, true, state) : null; const newCallbackParameter = new CallbackParameter(response.language.csharp?.name || '', responseType, headerType, this.state, { description: response.language.csharp?.description }); if (!withResult) { this.addParameter(newCallbackParameter); } this.callbacks.push(newCallbackParameter); } // add eventhandler parameter this.contextParameter = this.addParameter(new Parameter('eventListener', ClientRuntime.IEventListener, { description: `an <see cref="${ClientRuntime.IEventListener}" /> instance that will receive events.` })); // add optional parameter for sender this.senderParameter = this.addParameter(new Parameter('sender', ClientRuntime.ISendAsync, { description: `an instance of an ${ClientRuntime.ISendAsync} pipeline to use to make the request.` })); let rx = this.operation.requests ? this.operation.requests[0].protocol.http?.path : ''; const path = rx; let pathWithoutOperation = path; // For post API, Some URI may contain an action string .e.x '/start' at the end // of the URI, for such cases, we will drop the action string if identityCorrection // is set in the configuration if (this.operation.requests && this.operation.requests.length && this.operation.requests[0].protocol.http?.method === 'post' && this.state.project.identityCorrection) { const idx = rx.lastIndexOf('/'); rx = rx.substr(0, idx); pathWithoutOperation = rx; } let url = `/${path.startsWith('/') ? path.substr(1) : path}`; const serverParams = this.methodParameters.filter(each => each.param.protocol.http?.in === ParameterLocation.Uri); const headerParams = this.methodParameters.filter(each => each.param.protocol.http?.in === ParameterLocation.Header); const pathParams = this.methodParameters.filter(each => each.param.protocol.http?.in === ParameterLocation.Path); const queryParams = this.methodParameters.filter(each => each.param.protocol.http?.in === ParameterLocation.Query); const cookieParams = this.methodParameters.filter(each => each.param.protocol.http?.in === ParameterLocation.Cookie); // replace any server params in the uri for (const pp of serverParams) { url = url.replace(`{${pp.param.language.default.serializedName}}`, `" + ${pp.name} + "`); } for (const pp of pathParams) { rx = rx.replace(`{${pp.param.language.default.serializedName}}`, `(?<${pp.param.language.default.serializedName}>[^/]+)`); if (this.viaIdentity) { url = url.replace(`{${pp.param.language.default.serializedName}}`, `" + ${pp.name} + "`); } else { url = url.replace(`{${pp.param.language.default.serializedName}}`, `" + ${removeEncoding(pp, '', KnownMediaType.UriParameter)} + "`); } } rx = `"^${rx}$"`; url = url.replace(/\s*\+ ""/gm, ''); const bp = this.bodyParameter; if (bp) { this.serializationMode = ClientRuntime.SerializationMode.IncludeCreateOrUpdate; if (operation.language.default.name === 'Patch') { this.serializationMode = ClientRuntime.SerializationMode.IncludeUpdate; } // add optional parameter for json serialization mode this.serializationModeParameter = this.addParameter(new Parameter('serializationMode', ClientRuntime.SerializationMode, { description: `Allows the caller to choose the depth of the serialization. See <see cref="${ClientRuntime.SerializationMode}"/>.`, defaultInitializer: this.serializationMode })); } // add method implementation... this.add(function* () { const eventListener = new EventListener($this.contextParameter, $this.state.project.emitSignals, withResult); yield EOL; if ($this.viaIdentity) { yield '// verify that Identity format is an exact match for uri'; yield EOL; const match = Local('_match', `${System.Text.RegularExpressions.Regex.new(rx, 'global::System.Text.RegularExpressions.RegexOptions.IgnoreCase').value}.Match(${identity.value})`); yield match.declarationStatement; yield If(`!${match}.Success`, `throw new global::System.Exception("Invalid identity for URI '${pathWithoutOperation}'");`); yield EOL; yield '// replace URI parameters with values from identity'; for (const pp of pathParams) { yield `var ${pp.name} = ${match.value}.Groups["${pp.param.language.default.serializedName}"].Value;`; } } yield '// construct URL'; const pathAndQueryV = Local('pathAndQuery', `${System.Text.RegularExpressions.Regex.declaration}.Replace( "${url}" ${queryParams.length > 0 ? '+ "?"' : ''}${queryParams.joinWith(pp => ` + ${removeEncoding(pp, pp.param.language.default.serializedName, KnownMediaType.QueryParameter)}`, ` + "&"` )} ,"\\\\?&*$|&*$|(\\\\?)&+|(&)&+","$1$2")`.replace(/\s*\+ ""/gm, '')); yield pathAndQueryV.declarationStatement; yield EOL; yield eventListener.signal(ClientRuntime.Events.URLCreated, pathAndQueryV.value); yield EOL; yield '// generate request object '; const urlV = new LocalVariable('_url', dotnet.Var, { initializer: System.Uri.new(`$"${baseUrl}{${pathAndQueryV.value}}"`) }); yield urlV.declarationStatement; const method = $this.operation.requests ? $this.operation.requests[0].protocol.http?.method : ''; yield `var request = ${System.Net.Http.HttpRequestMessage.new(`${ClientRuntime.fullName}.Method.${method.capitalize()}, ${urlV.value}`)};`; yield eventListener.signal(ClientRuntime.Events.RequestCreated, 'request.RequestUri.PathAndQuery'); yield EOL; if (length(headerParams) > 0) { yield '// add headers parameters'; for (const hp of headerParams) { if (hp.param.language.default.name === 'Content-Length') { // content length is set when the request body is set continue; } yield hp.serializeToContainerMember(KnownMediaType.Header, new LocalVariable('request.Headers', dotnet.Var), hp.param.language.default.serializedName, ClientRuntime.SerializationMode.None); } yield EOL; } yield eventListener.signal(ClientRuntime.Events.HeaderParametersAdded); if (bp) { yield '// set body content'; yield `request.Content = ${bp.serializeToContent(bp.mediaType, new LiteralExpression('serializationMode'))};`; yield `request.Content.Headers.ContentType = ${System.Net.Http.Headers.MediaTypeHeaderValue.Parse(bp.contentType)};`; yield eventListener.signal(ClientRuntime.Events.BodyContentSet); } if (viaJson) { yield '// set body content'; yield 'request.Content = new global::System.Net.Http.StringContent(jsonString, global::System.Text.Encoding.UTF8);'; yield 'request.Content.Headers.ContentType = global::System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json");'; yield eventListener.signal(ClientRuntime.Events.BodyContentSet); } yield '// make the call '; }); } emitCall(returnFromCall: boolean, withResult?: boolean) { // storage will return from the call for download, etc. if (returnFromCall) { this.returnType = System.Threading.Tasks.Task(System.Net.Http.HttpResponseMessage); } this.add(`${withResult ? 'return ' : ''}await this.${this.callName} (request, ${withResult ? '' : `${this.callbacks.joinWith(each => each.use, ',')},`}${this.contextParameter.use},${this.senderParameter.use}); `); // remove constant parameters and make them locals instead. this.insert('// Constant Parameters'); for (let i = length(this.parameters); i--; i < 0) { const p = this.parameters[i]; if (p && p.defaultInitializer && p.name !== 'serializationMode') { this.parameters.splice(i, 1); this.insert(new LocalVariable(p.name, dotnet.Var, { initializer: p.defaultInitializer })); } } } } export function ResolveResponseType(opMethod?: OperationMethod, operation?: Operation, state?: State,): EnhancedTypeDeclaration | undefined { let typeCount = 0; let responseType: EnhancedTypeDeclaration | undefined = undefined; if (opMethod) { opMethod.callbacks.filter(each => each.name !== 'onDefault').forEach(each => { if (each.responseType && responseType && each.responseType !== responseType) { typeCount++; } else if (each.responseType && !responseType) { responseType = each.responseType; typeCount = 1; } }); } else if (operation && state) { for (const response of [...values(operation.responses), ...values(operation.exceptions)].filter(each => each.language?.csharp?.name !== 'onDefault')) { const eachResponseType = (<BinaryResponse>response).binary ? new Binary(new BinarySchema(''), true) : ((<SchemaResponse>response).schema ? state.project.modelsNamespace.NewResolveTypeDeclaration(<NewSchema>((<SchemaResponse>response).schema), true, state) : undefined); if (eachResponseType && responseType && eachResponseType !== responseType) { typeCount++; } else if (eachResponseType && !responseType) { responseType = eachResponseType; typeCount = 1; } } } return typeCount === 1 ? responseType : undefined; } export class CallMethod extends Method { public returnNull = false; constructor(protected parent: Class, protected opMethod: OperationMethod, protected state: State, objectInitializer?: DeepPartial<OperationMethod>, withResult?: boolean) { super(`${opMethod.name}_Call`, withResult && ResolveResponseType(opMethod) ? System.Threading.Tasks.Task(ResolveResponseType(opMethod)) : System.Threading.Tasks.Task()); this.description = `Actual wire call for <see cref= "${opMethod.name}" /> method.`; this.returnsDescription = opMethod.returnsDescription; this.apply(objectInitializer); this.access = Access.Internal; this.async = Modifier.Async; this.push(Using('NoSynchronizationContext', '')); const $this = this; // add parameters // request, listener, sender const reqParameter = this.addParameter(new Parameter('request', System.Net.Http.HttpRequestMessage, { description: 'the prepared HttpRequestMessage to send.' })); if (!withResult) { opMethod.callbacks.forEach(each => this.addParameter(each)); } this.addParameter(opMethod.contextParameter); this.addParameter(opMethod.senderParameter); // add statements to this method this.add(function* () { const eventListener = new EventListener(opMethod.contextParameter, $this.state.project.emitSignals, withResult); const response = Local('_response', dotnet.Null, System.Net.Http.HttpResponseMessage); yield response; yield Try(function* () { const responder = function* () { // TODO: omit generating _contentType var if it will never be used // const contentType = new LocalVariable('_contentType', dotnet.Var, { initializer: `_response.Content.Headers.ContentType?.MediaType` }); const contentType = Local('_contentType', `${response}.Content.Headers.ContentType?.MediaType`); yield contentType; // add response handlers yield Switch(`${response}.StatusCode`, function* () { const responses = [...values(opMethod.operation.responses), ...values(opMethod.operation.exceptions)].sort(function (a, b) { return (<string>(a.protocol.http?.statusCodes[0])).localeCompare(<string>(b.protocol.http?.statusCodes[0])); }); for (const resp of responses) { const responseCode = resp.protocol.http?.statusCodes[0]; if (responseCode !== 'default'/*TODO: !== not found, handle other exception response */) { const leadNum = parseInt(responseCode[0]); // will use enum when it can, fall back to casting int when it can't if (withResult) { yield TerminalCase(System.Net.HttpStatusCode[responseCode] ? System.Net.HttpStatusCode[responseCode].value : `${System.Net.HttpStatusCode.declaration} n when((int)n >= ${leadNum * 100} && (int)n < ${leadNum * 100 + 100})`, $this.responsesEmitter($this, opMethod, [resp], response, eventListener, withResult)); } else { yield Case(System.Net.HttpStatusCode[responseCode] ? System.Net.HttpStatusCode[responseCode].value : `${System.Net.HttpStatusCode.declaration} n when((int)n >= ${leadNum * 100} && (int)n < ${leadNum * 100 + 100})`, $this.responsesEmitter($this, opMethod, [resp], response, eventListener, false)); } } else { if (withResult) { yield TerminalDefaultCase($this.responsesEmitter($this, opMethod, [resp], response, eventListener, withResult)); } else { yield DefaultCase($this.responsesEmitter($this, opMethod, [resp], response, eventListener, false)); } } } // missing default response? if (!opMethod.operation.exceptions) { // if no default, we need one that handles the rest of the stuff. yield TerminalDefaultCase(function* () { yield `throw new ${ClientRuntime.fullName}.UndeclaredResponseException(_response);`; }); } }); }; const originalUri = Local('_originalUri', new LiteralExpression(`${reqParameter.use}.RequestUri.AbsoluteUri`)); if ($this.opMethod.operation.language.csharp?.lro) { yield '// this operation supports x-ms-long-running-operation'; yield originalUri; } // try statements const sendTask = Local( 'sendTask', new LiteralExpression( `${opMethod.senderParameter.value}.SendAsync(${reqParameter.use}, ${opMethod.contextParameter.value})` ) ); yield sendTask; // delay sending BeforeCall event until URI has been replaced by HTTP pipeline yield eventListener.signal(ClientRuntime.Events.BeforeCall, reqParameter.use); if ($this.opMethod.operation.language.csharp?.lro) { yield eventListener.signal(ClientRuntime.Events.Progress, new LiteralExpression('"intentional placeholder"'), new LiteralExpression('0')); } yield `${response.value} = await ${sendTask.value}; `; yield eventListener.signal(ClientRuntime.Events.ResponseCreated, response.value); const EOL = 'EOL'; // LRO processing (if appropriate) if ($this.opMethod.operation.language.csharp?.lro) { yield `// declared final-state-via: ${$this.opMethod.operation.language.csharp.lro['final-state-via']}`; const fsv = $this.opMethod.operation.language.csharp.lro['final-state-via']; let finalUri: LocalVariable; switch (fsv) { case 'original-uri': // perform a final GET on the original URI. finalUri = originalUri; break; case 'location': // perform a final GET on the uri in Location header finalUri = Local('_finalUri', response.invokeMethod('GetFirstHeader', new StringExpression('Location'))); yield finalUri; break; case 'operation-location': // perform a final GET on the uri in Operation-Location header finalUri = Local('_finalUri', response.invokeMethod('GetFirstHeader', new StringExpression('Operation-Location'))); yield finalUri; break; case 'azure-asyncoperation': case 'azure-async-operation': //depending on the type of request, do the appropriate behavior switch ($this.opMethod.operation.requests?.[0].protocol.http?.method.toLowerCase()) { case 'post': case 'delete': finalUri = Local('_finalUri', response.invokeMethod('GetFirstHeader', new StringExpression('Azure-AsyncOperation'))); yield finalUri; break; case 'patch': case 'put': // perform a final GET on the original URI. finalUri = originalUri; break; } break; default: // depending on the type of request, fall back to the appropriate behavior if ($this.opMethod.operation.requests) { switch ($this.opMethod.operation.requests[0].protocol.http?.method.toLowerCase()) { case 'post': case 'delete': finalUri = Local('_finalUri', response.invokeMethod('GetFirstHeader', new StringExpression('Location'))); yield finalUri; break; case 'patch': case 'put': // perform a final GET on the original URI. finalUri = originalUri; break; } } break; } const asyncOperation = Local('asyncOperation', response.invokeMethod('GetFirstHeader', new StringExpression('Azure-AsyncOperation'))); yield asyncOperation; const location = Local('location', response.invokeMethod('GetFirstHeader', new StringExpression('Location'))); yield location; const operationLocation = Local('operationLocation', response.invokeMethod('GetFirstHeader', new StringExpression('Operation-Location'))); yield operationLocation; yield While(new LiteralExpression(`${reqParameter.use}.Method == System.Net.Http.HttpMethod.Put && ${response.value}.StatusCode == ${System.Net.HttpStatusCode[200].value} || ${response.value}.StatusCode == ${System.Net.HttpStatusCode[201].value} || ${response.value}.StatusCode == ${System.Net.HttpStatusCode[202].value} `), function* () { yield '// delay before making the next polling request'; yield eventListener.signal(ClientRuntime.Events.DelayBeforePolling, response.value); yield EOL; yield '// while we wait, let\'s grab the headers and get ready to poll. '; yield 'if (!System.String.IsNullOrEmpty(_response.GetFirstHeader(@"Azure-AsyncOperation"))) {'; yield ' ' + asyncOperation.assign(response.invokeMethod('GetFirstHeader', new StringExpression('Azure-AsyncOperation'))); yield '}'; yield 'if (!global::System.String.IsNullOrEmpty(_response.GetFirstHeader(@"Location"))) {'; yield ' ' + location.assign(response.invokeMethod('GetFirstHeader', new StringExpression('Location'))); yield '}'; yield 'if (!global::System.String.IsNullOrEmpty(_response.GetFirstHeader(@"Operation-Location"))) {'; yield ' ' + operationLocation.assign(response.invokeMethod('GetFirstHeader', new StringExpression('Operation-Location'))); yield '}'; const uriLocal = Local('_uri', Ternery( System.String.IsNullOrEmpty(asyncOperation), Ternery(System.String.IsNullOrEmpty(location), Ternery(System.String.IsNullOrEmpty(operationLocation), originalUri, operationLocation ), location), asyncOperation)); yield uriLocal; yield `${reqParameter.use} = ${reqParameter.use}.CloneAndDispose(${System.Uri.new(uriLocal)}, ${ClientRuntime.Method.Get});`; yield EOL; yield '// and let\'s look at the current response body and see if we have some information we can give back to the listener'; const content = Local('content', new LiteralExpression(`await ${response.value}.Content.ReadAsStringAsync()`)); yield content; yield EOL; yield '// drop the old response'; yield `${response.value}?.Dispose();`; yield EOL; yield '// make the polling call'; yield `${response.value} = await ${opMethod.senderParameter}.SendAsync(${reqParameter.value}, ${opMethod.contextParameter});`; yield eventListener.signal(ClientRuntime.Events.Polling, response.value); yield EOL; yield ` // if we got back an OK, take a peek inside and see if it's done if( ${response.value}.StatusCode == ${System.Net.HttpStatusCode.OK}) { var error = false; try { if( ${ClientRuntime.JsonNode.Parse(toExpression(`await ${response.value}.Content.ReadAsStringAsync()`))} is ${ClientRuntime.JsonObject} json) { var state = json.Property("properties")?.PropertyT<${ClientRuntime.JsonString}>("provisioningState") ?? json.PropertyT<${ClientRuntime.JsonString}>("status"); if( state is null ) { // the body doesn't contain any information that has the state of the LRO // we're going to just get out, and let the consumer have the result break; } switch( state?.ToString()?.ToLower() ) { case "failed": error = true; break; case "succeeded": case "canceled": // we're done polling. break; default: // need to keep polling! ${response.value}.StatusCode = ${System.Net.HttpStatusCode.Created}; continue; } } } catch { // if we run into a problem peeking into the result, // we really don't want to do anything special. } if (error) { throw new ${ClientRuntime.fullName}.UndeclaredResponseException(${response.value}); } }`; yield EOL; yield '// check for terminal status code'; yield If(new LiteralExpression(`${response.value}.StatusCode == ${System.Net.HttpStatusCode[201].value} || ${response.value}.StatusCode == ${System.Net.HttpStatusCode[202].value} `), 'continue;'); yield '// we are done polling, do a request on final target?'; switch (fsv) { case 'original-uri': case 'azure-asyncoperation': case 'azure-async-operation': case 'location': case 'operation-location': // perform a final GET on the specified final URI. yield $this.finalGet(eventListener, finalUri, reqParameter, response); break; default: yield If(`!string.IsNullOrWhiteSpace(${finalUri})`, function* () { yield $this.finalGet(eventListener, finalUri, reqParameter, response); }); break; } }); } yield eventListener.signal(ClientRuntime.Events.Progress, new LiteralExpression('"intentional placeholder"'), new LiteralExpression('100')); yield responder(); }); yield Finally(function* () { yield '// finally statements'; yield eventListener.signalNoCheck(ClientRuntime.Events.Finally, 'request', '_response'); yield `${response.value}?.Dispose();`; yield `${reqParameter.use}?.Dispose();`; }); if ($this.returnNull) { yield Return('result'); $this.insert(new LocalVariable('result', System.Net.Http.HttpResponseMessage, { initializer: dotnet.Null })); } }); this.opMethod.emitCall($this.returnNull, withResult); } private * finalGet(eventListener: EventListener, finalLocation: ExpressionOrLiteral, reqParameter: Variable, response: Variable) { yield '// create a new request with the final uri'; yield reqParameter.assign(`${valueOf(reqParameter)}.CloneAndDispose(${System.Uri.new(finalLocation)}, ${ClientRuntime.Method.Get})`); yield EOL; yield '// drop the old response'; yield `${response.value}?.Dispose();`; yield EOL; yield '// make the final call'; yield response.assign(`await ${this.opMethod.senderParameter}.SendAsync(${valueOf(reqParameter)}, ${this.opMethod.contextParameter})`); yield eventListener.signal(ClientRuntime.Events.Polling, response.value); // make sure we're not polling anymore. yield 'break;'; } private * responsesEmitter($this: CallMethod, opMethod: OperationMethod, responses: Array<Response>, responseVariable: LocalVariable, eventListener: EventListener, withResult?: boolean) { if (length(responses) > 1) { yield Switch('_contentType', function* () { for (const eachResponse of values(responses)) { const mimetype = length(eachResponse.protocol.http?.mediaTypes) > 0 ? eachResponse.protocol.http?.mimeTypes[0] : ''; const callbackParameter = <CallbackParameter>values(opMethod.callbacks).first(each => each.name === eachResponse.language.csharp?.name); let count = length(eachResponse.protocol.http?.mediaTypes); for (const mt of values(eachResponse.protocol.http?.mediaTypes)) { count--; const mediaType = normalizeMediaType(<string>mt); if (mediaType) { if (count === 0) { yield Case(new StringExpression(mediaType).toString(), $this.NewResponseHandler(mimetype, eachResponse, callbackParameter, responseVariable, withResult)); } else { yield TerminalCase(new StringExpression(mediaType).toString(), ''); } } } } }); } else { const response = responses[0]; const callbackParameter = <CallbackParameter>values(opMethod.callbacks).first(each => each.name === response.language.csharp?.name); // all mimeTypes per for this response code. yield eventListener.signal(ClientRuntime.Events.BeforeResponseDispatch, responseVariable.value); yield $this.NewResponseHandler(<string>values(response.protocol.http?.mediaTypes).first() || '', response, callbackParameter, responseVariable, withResult); } } private * responseHandlerForNormalPipeline(mimetype: string, eachResponse: NewResponse, callbackParameter: CallbackParameter) { const callbackParameters = new Array<ExpressionOrLiteral>(); if (callbackParameter.responseType) { // hande the body response const r = callbackParameter.responseType.deserializeFromResponse(knownMediaType(mimetype), toExpression('_response'), toExpression('null')); if (r) { callbackParameters.push(r); } // if (parseMediaType(mimetype)) { // this media type isn't directly supported by deserialization // we can return a stream to the consumer instead // } } if (callbackParameter.headerType) { // header model deserialization... const r = callbackParameter.headerType.deserializeFromResponse(KnownMediaType.Header, toExpression('_response'), toExpression('null')); if (r) { callbackParameters.push(r); } } // make the callback with the appropriate parameters yield `await ${eachResponse.details.csharp.name}(_response${callbackParameters.length === 0 ? '' : ','}${callbackParameters.joinWith(valueOf)});`; } private * NewResponseHandlerForNormalPipeline(mimetype: string, eachResponse: Response, callbackParameter: CallbackParameter, responseVariable: LocalVariable) { const callbackParameters = new Array<ExpressionOrLiteral>(); if (callbackParameter.responseType) { // hande the body response const r = callbackParameter.responseType.deserializeFromResponse(knownMediaType(mimetype), toExpression(responseVariable.value), toExpression('null')); if (r) { callbackParameters.push(r); } // if (parseMediaType(mimetype)) { // this media type isn't directly supported by deserialization // we can return a stream to the consumer instead // } } if (callbackParameter.headerType) { // header model deserialization... const r = callbackParameter.headerType.deserializeFromResponse(KnownMediaType.Header, toExpression(responseVariable.value), toExpression('null')); if (r) { callbackParameters.push(r); } } // make the callback with the appropriate parameters yield `await ${eachResponse.language.csharp?.name}(_response${callbackParameters.length === 0 ? '' : ','}${callbackParameters.joinWith(valueOf)});`; } private * NewResponseHandlerWithResult(mimetype: string, response: Response, callbackParameter: CallbackParameter, responseVariable: LocalVariable) { const responseType = callbackParameter.responseType; if (!responseType) { yield `throw new ${ClientRuntime.fullName}.UndeclaredResponseException(${responseVariable.value});`; return; } const deserializeResponseAsync = responseType.deserializeFromResponse(knownMediaType(mimetype), toExpression(responseVariable.value), toExpression('null')); yield `var _result = ${deserializeResponseAsync};`; switch (response.protocol.http?.statusCodes[0]) { case 'default': { // this should write an error to the error channel. yield `// Error Response : ${response.protocol.http?.statusCodes[0]}`; const unexpected = function* () { yield '// Unrecognized Response. Create an error record based on what we have.'; const ex = responseType ? Local('ex', `new ${ClientRuntime.name}.RestException<${responseType}>(${responseVariable.value}, await _result)`) : Local('ex', `new ${ClientRuntime.name}.RestException(responseMessage)`); yield ex.declarationStatement; yield `throw ${ex};`; }; if ((<SchemaResponse>response).schema !== undefined) { // the schema should be the error information. // this supports both { error { message, code} } and { message, code} let props = NewGetAllPublicVirtualProperties((<SchemaResponse>response).schema.language.csharp?.virtualProperties); const errorProperty = values(props).first(p => p.property.serializedName === 'error'); let ep = ''; if (errorProperty) { props = NewGetAllPublicVirtualProperties(errorProperty.property.schema.language.csharp?.virtualProperties); ep = `${errorProperty.name}?.`; } const codeProp = props.find(p => p.name.toLowerCase().indexOf('code') > -1); // first property with 'code' const messageProp = props.find(p => p.name.toLowerCase().indexOf('message') > -1); // first property with 'message' const actionProp = props.find(p => p.name.toLowerCase().indexOf('action') > -1); // first property with 'action' if (codeProp && messageProp) { const lcode = new LocalVariable('code', dotnet.Var, { initializer: `(await _result)?.${ep}${codeProp.name}` }); const lmessage = new LocalVariable('message', dotnet.Var, { initializer: `(await _result)?.${ep}${messageProp.name}` }); const laction = actionProp ? new LocalVariable('action', dotnet.Var, { initializer: `(await _result)?.${ep}${actionProp.name} ?? ${System.String.Empty}` }) : undefined; yield lcode; yield lmessage; yield laction; yield If(Or(IsNull(lcode), (IsNull(lmessage))), unexpected); yield Else(`throw new global::System.Exception($"[{${lcode}}] : {${lmessage}}");`); } else { yield unexpected; } } else { yield unexpected; } break; } default: { yield 'return await _result;'; break; } } } private responseHandler(mimetype: string, eachResponse: NewResponse, callbackParameter: CallbackParameter) { return this.responseHandlerForNormalPipeline(mimetype, eachResponse, callbackParameter); } private NewResponseHandler(mimetype: string, eachResponse: Response, callbackParameter: CallbackParameter, responseVariable: LocalVariable, withResult?: boolean) { return withResult ? this.NewResponseHandlerWithResult(mimetype, eachResponse, callbackParameter, responseVariable) : this.NewResponseHandlerForNormalPipeline(mimetype, eachResponse, callbackParameter, responseVariable); } } export class ValidationMethod extends Method { constructor(protected parent: Class, protected opMethod: OperationMethod, protected state: State, objectInitializer?: DeepPartial<OperationMethod>) { super(`${opMethod.name}_Validate`, System.Threading.Tasks.Task()); this.apply(objectInitializer); this.description = `Validation method for <see cref="${opMethod.name}" /> method. Call this like the actual call, but you will get validation events back.`; this.returnsDescription = opMethod.returnsDescription; this.access = Access.Internal; this.async = Modifier.Async; this.push(Using('NoSynchronizationContext', '')); // add the method parameters for (const parameter of opMethod.methodParameters) { if (!parameter.defaultInitializer) { this.addParameter(parameter); } } if (opMethod.bodyParameter) { this.addParameter(opMethod.bodyParameter); } this.addParameter(opMethod.contextParameter); // add statements to this method this.add(function* () { for (const parameter of opMethod.methodParameters) { if (!parameter.defaultInitializer) { // spit out parameter validation yield parameter.validatePresenceStatement(opMethod.contextParameter); yield parameter.validationStatement(opMethod.contextParameter); } } // spit out body parameter validation too if (opMethod.bodyParameter) { yield opMethod.bodyParameter.validatePresenceStatement(opMethod.contextParameter); yield opMethod.bodyParameter.validationStatement(opMethod.contextParameter); } }); } }