powershell/plugins/create-commands-v2.ts (518 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 { HttpMethod, codeModelSchema, CodeModel, ObjectSchema, GroupSchema, isObjectSchema, SchemaType, GroupProperty, ParameterLocation, Operation, Parameter, VirtualParameter, getAllProperties, ImplementationLocation, OperationGroup, Request, SchemaContext, SchemaResponse } from '@autorest/codemodel'; import { deconstruct, fixLeadingNumber, pascalCase, EnglishPluralizationService, fail, removeSequentialDuplicates, serialize, includeXDash } from '@azure-tools/codegen'; import { items, values, keys, Dictionary, length } from '@azure-tools/linq'; import { Schema } from '../llcsharp/exports'; import { Channel, AutorestExtensionHost as Host, Session, startSession } from '@autorest/extension-base'; import { Lazy } from '@azure-tools/tasks'; import { clone } from '@azure-tools/linq'; import { verbs } from '../internal/verbs'; import { PwshModel } from '../utils/PwshModel'; import { IParameter } from '../utils/components'; import { ModelState } from '../utils/model-state'; //import { Schema as SchemaV3 } from '../utils/schema'; import { CommandOperation, CommandType, VirtualParameter as CommandVirtualParameter } from '../utils/command-operation'; import { OperationType } from '../utils/command-operation'; import { Schema as SchemaModel } from '@autorest/codemodel'; import { getResourceNameFromPath } from '../utils/resourceName'; import { hasValidBodyParameters } from '../utils/http-operation'; type State = ModelState<PwshModel>; const specialWords: { [key: string]: Array<string> } = { in: <Array<string>>['sign'] }; // UNUSED: Moved to plugin-tweak-model.ts in remodeler // For now, we are not dynamically changing the service-name. Instead, we would figure out a method to change it during the creation of service readme's. export function titleToAzureServiceName(title: string): string { const titleCamelCase = pascalCase(deconstruct(title)).trim(); const serviceName = titleCamelCase // Remove: !StartsWith(Management)AndContains(Management), Client, Azure, Microsoft, APIs, API, REST .replace(/(?!^Management)(?=.*)Management|Client|Azure|Microsoft|APIs|API|REST/g, '') // Remove: EndsWith(ServiceResourceProvider), EndsWith(ResourceProvider), EndsWith(DataPlane), EndsWith(Data) .replace(/ServiceResourceProvider$|ResourceProvider$|DataPlane$|Data$/g, ''); return serviceName || titleCamelCase; } const pluralizationService = new EnglishPluralizationService(); pluralizationService.addWord('Database', 'Databases'); pluralizationService.addWord('database', 'databases'); pluralizationService.addWord('Premise', 'Premises'); pluralizationService.addWord('premise', 'premises'); interface CommandVariant { alias: Array<string>; verb: string; subject: string; subjectPrefix: string; variant: string; action: string; } function fn<T>(active: Array<T>, remaining: Array<T>, result: Array<Array<T>>): Array<Array<T>> { if (length(active) || length(remaining)) { if (length(remaining)) { fn([...active, remaining[0]], remaining.slice(1), result); fn(active, remaining.slice(1), result); } else { result.push(active); } } return result; } function combinations<T>(elements: Array<T>) { return fn([], elements, []); } function filterSpecialWords(preposition: string, parts: Array<string>) { let start = 0; let idx = -1; if (specialWords[preposition] !== undefined) { do { idx = parts.indexOf(preposition, start); if (idx <= 0 || !specialWords[preposition].includes(parts[idx - 1])) { return idx; } else { start = idx + 1; } // eslint-disable-next-line no-constant-condition } while (true); } return parts.indexOf(preposition); } function splitOnPreposition(preposition: string, parts: Array<string>) { const i = filterSpecialWords(preposition, parts); if (i > 0) { return [ parts.slice(0, i), parts.slice(i + 1) ]; } return undefined; } function splitOnAnyPreposition(parts: Array<string>) { for (const p of ['with', 'at', 'by', 'for', 'in', 'of']) { const result = splitOnPreposition(p, parts); if (result && result[0].length > 0) { // we found it, let's give it back. return result; } } return undefined; } export /* @internal */ class Inferrer { commonParameters = new Array<string>(); verbMap!: { [operationIdMethod: string]: string }; prefix!: string; serviceName!: string; subjectPrefix!: string; isAzure!: boolean; supportJsonInput!: boolean; constructor(private state: State) { } async init() { this.commonParameters = await this.state.getValue('azure', false) ? [ // 'resourceGroupName', 'subscriptionId' ] : []; this.verbMap = await this.state.getValue('verb-mapping') || {}; this.isAzure = await this.state.getValue('azure', false); this.prefix = await this.state.getValue('prefix'); this.serviceName = titleToAzureServiceName(await this.state.getValue('service-name')); if (this.isAzure) { this.supportJsonInput = await this.state.getValue('support-json-input', true); } else { this.supportJsonInput = await this.state.getValue('support-json-input', false); } this.state.setValue('service-name', this.serviceName); this.subjectPrefix = await this.state.getValue('subject-prefix'); this.state.setValue('isAzure', this.isAzure); this.state.setValue('prefix', this.prefix); const model = this.state.model; this.state.message({ Channel: Channel.Debug, Text: `[CMDLET-PREFIX] => '${model.language.default.prefix}'` }); model.language.default.serviceName = this.serviceName; this.state.message({ Channel: Channel.Debug, Text: `[SERVICE-NAME] => '${model.language.default.serviceName}'` }); model.language.default.subjectPrefix = this.subjectPrefix; this.state.message({ Channel: Channel.Debug, Text: `[SUBJECT-PREFIX] => '${model.language.default.subjectPrefix}'` }); return this; } async createCommands() { const model = this.state.model; this.state.model.commands = <any>{ operations: new Dictionary<any>(), parameters: new Dictionary<any>(), }; const disableGetPut = await this.state.getValue('disable-getput', false); const disableTransformIdentityType = await this.state.getValue('disable-transform-identity-type', false); const disableTransformIdentityTypeForOperation = await this.state.getValue('disable-transform-identity-type-for-operation', []); const optsToExclude = new Array<string>(); if (disableTransformIdentityTypeForOperation) { for (const item of values(disableTransformIdentityTypeForOperation)) { optsToExclude.push(item); } } this.state.message({ Channel: Channel.Debug, Text: 'detecting high level commands...' }); for (const operationGroup of values(model.operationGroups)) { let hasPatch = false; let commandType!: CommandType; const getOperations: Array<Operation> = []; let putOperation: Operation | undefined; let patchOperation: Operation | undefined; for (const operation of values(operationGroup.operations)) { if (operation.requests?.[0]?.protocol?.http?.method.toLowerCase() === 'patch') { hasPatch = true; patchOperation = operation; // bez: remove patch operation to avoid conflicts with replacements if (this.isAzure && !disableTransformIdentityType && this.IsManagedIdentityOperation(operation)) { continue; } } else if (operation.requests?.[0]?.protocol?.http?.method.toLowerCase() === 'get') { getOperations.push(operation); } else if (operation.requests?.[0]?.protocol?.http?.method.toLowerCase() === 'put') { putOperation = operation; if (this.isAzure && !disableTransformIdentityType && this.IsManagedIdentityOperation(operation)) { commandType = CommandType.ManagedIdentityNew; } } for (const variant of await this.inferCommandNames(operation, operationGroup.$key, this.state)) { await this.addVariants(operation.parameters, operation, variant, '', this.state, undefined, commandType); } } // bez: if we have get+put, try to replace if (this.isAzure && getOperations && putOperation && putOperation.requests?.length == 1) { const getOperation = getOperations.find(getOperation => getOperation.requests?.[0]?.protocol?.http?.path === putOperation?.requests?.[0]?.protocol?.http?.path); const supportsCombineGetPutOperation = getOperation && this.supportsGetPut(getOperation, putOperation); if (!disableTransformIdentityType && supportsCombineGetPutOperation && (hasPatch && patchOperation && this.IsManagedIdentityOperation(patchOperation) || !hasPatch && putOperation && this.IsManagedIdentityOperation(putOperation))) { const variant = (await this.inferCommandNames(getOperation, operationGroup.$key, this.state))[0]; await this.addVariants(putOperation.parameters, putOperation, variant, '', this.state, [getOperation], CommandType.ManagedIdentityUpdate); } else if (!disableTransformIdentityType && !supportsCombineGetPutOperation && hasPatch && patchOperation && this.IsManagedIdentityOperation(patchOperation)) { if (!optsToExclude.includes(patchOperation.operationId ?? '')) { const transformIdentityTypeErrorMessage = `Parameter IdentityType in operation '${patchOperation.operationId}' can not be transformed as the best practice design. See https://github.com/Azure/azure-powershell/blob/main/documentation/development-docs/design-guidelines/managed-identity-best-practices.md#frequently-asked-question to mitigate this issue.`; this.state.message({ Channel: Channel.Error, Text: transformIdentityTypeErrorMessage }); throw new Error(transformIdentityTypeErrorMessage); } // bez: add patch operation back and disable transforming identity type for (const variant of await this.inferCommandNames(patchOperation, operationGroup.$key, this.state)) { await this.addVariants(patchOperation.parameters, patchOperation, variant, '', this.state); } } else if (!disableGetPut && !hasPatch && supportsCombineGetPutOperation) { /* generate variants for Update(Get+Put) for subjects only if: - there is a get operation - there is a put operation - get operation path is the same as put operation path - there is only one put request schema - get operation response schema type is the same as put operation request schema type */ const variant = (await this.inferCommandNames(getOperation, operationGroup.$key, this.state))[0]; await this.addVariants(putOperation.parameters, putOperation, variant, '', this.state, [getOperation], CommandType.GetPut); } } else if (this.isAzure && !disableTransformIdentityType && patchOperation && this.IsManagedIdentityOperation(patchOperation)) { if (!optsToExclude.includes(patchOperation.operationId ?? '')) { const transformIdentityTypeErrorMessage = `Parameter IdentityType in operation '${patchOperation.operationId}' can not be transformed as the best practice design. See https://github.com/Azure/azure-powershell/blob/main/documentation/development-docs/design-guidelines/managed-identity-best-practices.md#frequently-asked-question to mitigate this issue.`; this.state.message({ Channel: Channel.Error, Text: transformIdentityTypeErrorMessage }); throw new Error(transformIdentityTypeErrorMessage); } // bez: add variants back and disable transforming identity type as no put or get for (const variant of await this.inferCommandNames(patchOperation, operationGroup.$key, this.state)) { await this.addVariants(patchOperation.parameters, patchOperation, variant, '', this.state); } } } // for (const operation of values(model.http.operations)) { // for (const variant of await this.inferCommandNames(operation, this.state)) { // // no common parameters (standard variations) // await this.addVariants(operation.parameters, operation, variant, '', this.state); // } // } return model; } /** * Judge if the response of get operation can be piped as the input of put operation * 1. there is only one put request schema * 2. get operation response schema type is the same as put operation request schema type */ private supportsGetPut(getOperation: Operation, putOperation: Operation): boolean { const hasQueryParameter = getOperation?.parameters?.find(p => p.protocol.http?.in === 'query' && p.language.default.name !== 'apiVersion'); //parameter.protocal.http.in === 'body' probably only applies to open api 2.0 const schema = putOperation?.requests?.[0]?.parameters?.find(p => p.protocol.http?.in === 'body')?.schema; return (getOperation && !hasQueryParameter && schema && [...values(getOperation?.responses)].filter(each => (<SchemaResponse>each).schema !== schema).length === 0) ?? false; } private containsIdentityType(op: Operation): boolean { const body = op.requests?.[0].parameters?.find((p) => !p.origin || p.origin.indexOf('modelerfour:synthesized') < 0) || null; // property identity in the body parameter const identityProperty = (body && body.schema && isObjectSchema(body.schema)) ? values(getAllProperties(body.schema)).where(property => !property.language.default.readOnly && isObjectSchema(property.schema) && property.language.default.name === 'identity')?.toArray()?.[0] : null; const identityTypeProperty = (identityProperty && identityProperty.schema && isObjectSchema(identityProperty.schema)) ? values(getAllProperties(identityProperty.schema)).where(property => !property.language.default.readOnly && property.language.default.name === 'type')?.toArray()?.[0] : null; return identityTypeProperty !== null && identityTypeProperty !== undefined; } private IsManagedIdentityOperation(op: Operation): boolean { return this.containsIdentityType(op); } inferCommand(operation: Array<string>, group: string, suffix: Array<string> = []): Array<CommandVariant> { operation = operation.filter(each => each !== 'all'); // no instant match switch (length(operation)) { case 0: throw new Error('Missing operation id?'); case 1: // simple operation, just an id with a single value // OPERATION => OPERATION-GROUP return [ this.createCommandVariant(operation[0], [group], suffix, this.state.model) ]; case 2: // should try to infer [SUBJECT] and [ACTION] from operation if (verbs.has(operation[0])) { // [ACTION][SUBJECT] return [ this.createCommandVariant(operation[0], [group, operation[1]], suffix, this.state.model) ]; } if (verbs.has(operation[1])) { // [SUBJECT][ACTION] return [ this.createCommandVariant(operation[1], [group, operation[0]], suffix, this.state.model) ]; } // can't tell which is the [ACTION] -- let's return it the way we used to, // but now let's mention that we are doing that. this.state.warning(`Operation ${operation[0]}/${operation[1]} is inferred without finding action.`, [], {}); return [ this.createCommandVariant(operation[0], [group, operation[1]], suffix, this.state.model) ]; } // three or more words. // first, see if it's an 'or' const either = splitOnPreposition('or', operation); if (either) { // looks like we got two sets of operations from this. return [ ...this.inferCommand([...either[0], ...either[1].slice(1)], group, suffix), ...this.inferCommand(either[1], group, suffix), ]; } // any other preposition? const parts = splitOnAnyPreposition(operation); if (parts) { // so this is something like DoActionWithStyle return [...this.inferCommand(parts[0], group, parts[1])]; } // if not, then seek out a verb from there. for (let i = 0; i < length(operation); i++) { if (verbs.has(operation[i])) { // if the action is first if (i === 0) { // everything else is the subject return [this.createCommandVariant(operation[i], group ? [group, ...operation.slice(i + 1)] : operation.slice(i + 1), suffix, this.state.model)]; } if (i === length(operation) - 1) { // if it's last, the subject would be the first thing return [this.createCommandVariant(operation[i], group ? [group, ...operation.slice(0, i)] : operation.slice(0, i), suffix, this.state.model)]; } // otherwise // things before are part of the subject // things after the verb should be part of the suffix return [this.createCommandVariant(operation[i], group ? [group, ...operation.slice(i, i)] : operation.slice(i, i), [...suffix, ...operation.slice(i + 1)], this.state.model)]; } } // so couldn't tell what the action was. // fallback to the original behavior with a warning. this.state.warning(`Operation ${operation[0]}/${operation[1]} is inferred without finding action.`, [], {}); return [this.createCommandVariant(operation[0], group ? [group, ...operation.slice(1)] : operation.slice(1), [...suffix, ...operation.slice(1)], this.state.model)]; } async inferCommandNames(op: Operation, group: string, state: State): Promise<Array<CommandVariant>> { const method = op.language.default.name; // skip-for-time-being // if (!method) { // if (!group) { // // no operation id at all? // const path = op.path.replace(/{.*?}/g, '').replace(/\/+/g, '/').replace(/\/$/g, ''); // method = path.split('/').last; // } else { // // no group given, use string as method // method = group; // } // group = pascalCase(op.tags) || ''; // } const groupWords = [...removeSequentialDuplicates(deconstruct(group))]; groupWords[groupWords.length - 1] = pluralizationService.singularize(groupWords.last); group = pascalCase(groupWords); const operation = deconstruct(method); // instant match if (this.verbMap[method]) { return [this.createCommandVariant(method, [group], [], state.model)]; } // dig deeper. return this.inferCommand(operation, group); } async addVariant(vname: string, body: Parameter | null, bodyParameterName: string, parameters: Array<Parameter>, operation: Operation, variant: CommandVariant, state: State, preOperations: Array<Operation> | undefined, commandType?: CommandType): Promise<CommandOperation> { // beth: filter command description for New/Update command // positive test case: The operation to create or update the extension. // negative test case: Starts an UpdateRun const createOrUpdateRegex = /((create or update)|(creates or updates)|(create)|(update)|(creates)|(updates))([^a-zA-Z0-9])/gi; operation.language.default.description = operation.language.default.description.replace(createOrUpdateRegex, `${variant.action.toLowerCase()} `); const op = await this.addCommandOperation(vname, parameters, operation, variant, state, preOperations, commandType); // if this has a body with it, let's add that parameter if (body && body.schema) { op.details.default.hasBody = true; op.parameters.push(new IParameter(bodyParameterName, body.schema, { required: body.required, details: { default: { description: body.schema.language.default.description, name: pascalCase(bodyParameterName), isBodyParameter: true, } } })); // let's add a variant where it's expanded out. // *IF* the body is an object or dictionary if (body.schema.type === SchemaType.Object || body.schema.type === SchemaType.Dictionary || body.schema.type === SchemaType.Any) { const opExpanded = await this.addCommandOperation(`${vname}Expanded`, parameters, operation, variant, state, preOperations, commandType); opExpanded.details.default.dropBodyParameter = true; opExpanded.parameters.push(new IParameter(`${bodyParameterName}Body`, body.schema, { required: body.required, details: { default: { description: body.schema.language.default.description, name: pascalCase(`${bodyParameterName}Body`), isBodyParameter: true, } } })); } } return op; } // skip-for-time-being // isNameConflict(model: codemodel.Model, variant: CommandVariant, vname: string) { // for (const each of values(model.commands.operations)) { // if (each.details.default.name === vname) { // return true; // } // } // return false; // } // for tracking unique operation identities operationIdentities = new Set<string>(); async addCommandOperation(vname: string, parameters: Array<Parameter>, operation: Operation, variant: CommandVariant, state: State, preOperations: Array<Operation> | undefined, commandType?: CommandType): Promise<CommandOperation> { // skip-for-time-being following code seems redundant ----- // let apiversion = ''; // for (const each of items(operation.responses)) { // for (const rsp of items(each)) { // if (rsp.schema && rsp.schema.details && rsp.schema.details.default && rsp.schema.details.default.apiversion) { // apiversion = rsp.schema.details.default.apiversion; // break; // } // } // } // ---------------------------------------------------------- // if vname is > 64 characters, let's trim it // after trimming it, make sure there aren't any other operation with a name that's exactly the same if (length(vname) > 64) { const names = deconstruct(vname); let newVName = ''; for (let i = 0; i < length(names); i++) { newVName = pascalCase(names.slice(0, i)); if (length(newVName) > 60) { break; } } vname = newVName; } // if we have an identical vname, let's add 'etc' let fname = `${variant.verb}-${variant.subject}-${vname}`; let n = 1; while (this.operationIdentities.has(fname)) { vname = `${vname.replace(/\d*$/g, '')}${n++}`; fname = `${variant.verb}-${variant.subject}-${vname}`; } this.operationIdentities.add(fname); variant.variant = vname; vname = pascalCase(vname); let commandName = operation.language.default.name; let operations = [operation]; const language = { default: { ...operation.language.default } }; //handle when there are pre operataions if (preOperations && preOperations.length > 0) { commandName = 'Update'; language.default.name = 'Update'; operations = [...preOperations, ...operations]; } // skip-for-time-being x-ms-metadata looks not supported any more. //const xmsMetadata = operation.pathExtensions ? operation.pathExtensions['x-ms-metadata'] ? clone(operation.pathExtensions['x-ms-metadata']) : {} : {}; // Add operation type to support x-ms-mutability let operationType = OperationType.Other; if (operation.requests) { if (operation.requests[0].protocol.http?.method === 'put' && (variant.action.toLowerCase() === 'create' || variant.action.toLowerCase() === 'update' && variant.verb.toLowerCase() === 'set')) { // put create and put set operationType = OperationType.Create; } else if ((operation.requests[0].protocol.http?.method === 'patch' || operation.requests[0].protocol.http?.method === 'put') && variant.action.toLowerCase() === 'update') { // patch update, get+put update and exclude set update operationType = OperationType.Update; } } return state.model.commands.operations[`${length(state.model.commands.operations)}`] = new CommandOperation(commandName, { asjob: language.default.asjob ? true : false, operationType: operationType, extensions: { }, ...variant, details: { ...language, default: { ...language.default, subject: variant.subject, subjectPrefix: variant.subjectPrefix, verb: variant.verb, name: vname, alias: variant.alias, externalDocs: operation.externalDocs } }, // operationId is not needed any more operationId: '', parameters: parameters.map(httpParameter => { // make it's own copy of the parameter since after this, // the parameter can be altered for each operation individually. const each = clone(httpParameter, false, undefined, undefined, ['schema', 'origin']); each.language.default = { ...each.language.default, name: pascalCase(each.language.default.name), httpParameter }; each.details = {}; each.details.default = { ...each.language.default, name: pascalCase(each.language.default.name), httpParameter }; each.name = each.language.default.serializedName; return each; }), // skip-for-time-being, this callGraph is used in the header comments callGraph: operations }, commandType); } async addVariants(parameters: Array<Parameter> | undefined, operation: Operation, variant: CommandVariant, vname: string, state: State, preOperations?: Array<Operation>, commandType?: CommandType) { // now synthesize parameter set variants multiplexed by the variants. const [constants, requiredParameters] = values(parameters).bifurcate(parameter => parameter.language.default.constantValue || parameter.language.default.fromHost ? true : false); const constantParameters = constants.map(each => `'${each.language.default.constantValue}'`); // the body parameter // xichen: How to handle if has multiple requests? const body = operation.requests?.[0].parameters?.find((p) => !p.origin || p.origin.indexOf('modelerfour:synthesized') < 0) || null; // skip-for-time-being, looks x-ms-requestBody-name is not supported any more //const bodyParameterName = (operation.requestBody && operation.requestBody.extensions) ? operation.requestBody.extensions['x-ms-requestBody-name'] || 'bodyParameter' : ''; const bodyParameterName = body ? body.language.default.name : ''; // all the properties in the body parameter const bodyProperties = (body && body.schema && isObjectSchema(body.schema)) ? values(getAllProperties(body.schema)).where(property => !property.language.default.readOnly).toArray() : []; // smash body property names together const bodyPropertyNames = bodyProperties.joinWith(each => each.language.default.name); // for each polymorphic body, we should do a separate variant that takes the polymorphic body type instead of the base type // skip-for-time-being, this is for polymorphism //const polymorphicBodies = (body && body.schema && body.schema.details.default.polymorphicChildren && length(body.schema.details.default.polymorphicChildren)) ? (<Array<Schema>>body.schema.details.default.polymorphicChildren).joinWith(child => child.details.default.name) : ''; // wait! "update" should be "set" if it's a PUT if (variant.verb === 'Update' && operation.requests && operation.requests[0].protocol?.http?.method === HttpMethod.Put) { variant.verb = 'Set'; } //for operations which has pre operations (only support GET+PUT for now), change it's action to update if (preOperations && preOperations.length > 0) { variant.action = variant.verb = 'Update'; } // create variant // skip-for-time-being, since operationId looks not included in m4. //state.message({ Channel: Channel.Debug, Text: `${variant.verb}-${variant.subject} // ${operation.operationId} => ${JSON.stringify(variant)} taking ${requiredParameters.joinWith(each => each.name)}; ${constantParameters} ; ${bodyPropertyNames} ${polymorphicBodies ? `; Polymorphic bodies: ${polymorphicBodies} ` : ''}` }); await this.addVariant(pascalCase([variant.action, vname]), body, bodyParameterName, [...constants, ...requiredParameters], operation, variant, state, preOperations, commandType); if (await state.getValue('disable-via-identity', false)) { return; } // eslint-disable-next-line prefer-const let [pathParams, otherParams] = values(requiredParameters).bifurcate(each => each?.protocol?.http?.in === ParameterLocation.Path); //exclude subscriptionId and resourceGroupName from path parameters pathParams = pathParams.filter(pathParam => !this.reservedPathParam.has(pathParam.language.default.name)); //if parent pipline input is disabled, only generate identity for current resource itself if (!await state.getValue('enable-parent-pipeline-input', this.isAzure)) { if (length(pathParams) > 0 && variant.action.toLowerCase() != 'list') { await this.addVariant(pascalCase([variant.action, vname, 'via-identity']), body, bodyParameterName, [...constants, ...otherParams], operation, variant, state, preOperations, commandType); } return; } const disableGetEnableList = await this.state.getValue('enable-parent-pipeline-input-for-list', false); /* for resource /A1/A2/.../An-1/An, generate variants that take ViaIdentity: An as identity ViaIdentity{An-1}: An-1 as identity + An Name ... ViaIdentity{A1}: A1 as identity + [A2 + A3 + ... + An-1 + An] Names */ for (let i = pathParams.length - 1; i >= 0; i--) { if ((!disableGetEnableList && variant.action.toLowerCase() === 'list') || (disableGetEnableList && i === pathParams.length - 1 && variant.action.toLowerCase() === 'get')) { continue; } let resourceName = getResourceNameFromPath(operation.requests?.[0].protocol.http?.path, pathParams[i].language.default.name, true); //cannot get resource name from path, give up generate ViaIdentity variant if (!resourceName) { break; } //variant for current resource is simply named ViaIdentity otherwise ViaIdentity${resourceName} if (i === pathParams.length - 1 && variant.action.toLowerCase() !== 'list') { resourceName = ''; } await this.addVariant(pascalCase([variant.action, vname, `via-identity${resourceName}`]), body, bodyParameterName, [...constants, ...otherParams, ...pathParams.slice(i + 1)], operation, variant, state, preOperations, commandType); } if (this.supportJsonInput && hasValidBodyParameters(operation) && commandType != CommandType.GetPut && commandType != CommandType.ManagedIdentityUpdate) { const createStringParameter = (name: string, description: string, serializedName: string): IParameter => { const schema = new SchemaModel(name, description, SchemaType.String); const language = { default: { name: name, description: description, serializedName: serializedName, }, }; schema.language = language; const httpParameter = { implementation: 'Method', language: language, schema: schema, required: true, }; const parameter = new IParameter(name, schema, { description: description, required: true, details: { default: { description: description, name: name, isBodyParameter: false, httpParameter: httpParameter, }, }, schema: schema, allowEmptyValue: false, deprecated: false, }); (<any>parameter).httpParameter = httpParameter; return parameter; }; const jsonVariant = pascalCase([variant.action, vname]); const parameter = new IParameter(`${bodyParameterName}Body`, body!.schema, { details: { default: { description: body!.schema.language.default.description, name: pascalCase(`${bodyParameterName}Body`), isBodyParameter: true, } } }); const opJsonString = await this.addVariant(`${jsonVariant}ViaJsonString`, null, '', [...constants, ...requiredParameters], operation, variant, state, preOperations, commandType); opJsonString.details.default.dropBodyParameter = true; opJsonString.parameters = opJsonString.parameters.filter(each => each.details.default.isBodyParameter !== true); opJsonString.parameters.push(createStringParameter('JsonString', `Json string supplied to the ${jsonVariant} operation`, 'jsonString')); opJsonString.parameters.push(parameter); opJsonString.details.default.dropBodyParameter = true; const opJsonFilePath = await this.addVariant(`${jsonVariant}ViaJsonFilePath`, null, '', [...constants, ...requiredParameters], operation, variant, state, preOperations, commandType); opJsonFilePath.details.default.dropBodyParameter = true; opJsonFilePath.parameters = opJsonFilePath.parameters.filter(each => each.details.default.isBodyParameter !== true); opJsonFilePath.parameters.push(createStringParameter('JsonFilePath', `Path of Json file supplied to the ${jsonVariant} operation`, 'jsonFilePath')); opJsonFilePath.parameters.push(parameter); opJsonFilePath.details.default.dropBodyParameter = true; } } reservedPathParam = new Set<string>(['SubscriptionId', 'resourceGroupName']); createCommandVariant(action: string, subject: Array<string>, variant: Array<string>, model: PwshModel): CommandVariant { const verb = this.getPowerShellVerb(action); if (verb === 'Invoke') { // if the 'operation' name was "post" -- it's kindof redundant. // so, only include the operation name in the group name if it's anything else if (action.toLowerCase() !== 'post') { subject = [action, ...subject]; } } return { alias: [], subject: pascalCase([...removeSequentialDuplicates(subject.map(each => pluralizationService.singularize(each)))]), variant: pascalCase(variant), verb, subjectPrefix: model.language.default.subjectPrefix, action }; } getPowerShellVerb(action: string): string { const verb = this.verbMap[pascalCase(action)]; if (verb) { return verb; } else { return 'Invoke'; } } } export async function createCommandsV2(service: Host) { // return processCodeModel(commandCreator, service); //const session = await startSession<PwshModel>(service, {}, codeModelSchema); //const result = tweakModelV2(session); const state = await new ModelState<PwshModel>(service).init(); await service.writeFile({ filename: 'code-model-v4-createcommands-v2.yaml', content: serialize(await (await new Inferrer(state).init()).createCommands()), sourceMap: undefined, artifactType: 'code-model-v4' }); // return processCodeModel(async (state) => { // return await (await new Inferrer(state).init()).createCommands(); // }, service, 'createCommands-v2'); }