diff --git a/README.md b/README.md index 5a3df3b..3febbd2 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,12 @@ pipeline: input: modelerfour output-artifact: clicommon-prenamer - clicommon/pre/cli-complex-marker: + clicommon/cli-split-operation: input: clicommon/cli-prenamer + output-artifact: clicommon-split-operation + + clicommon/pre/cli-complex-marker: + input: clicommon/cli-split-operation output-artifact: clicommon-complex-marker-pre clicommon/cli-flatten-setter: @@ -52,6 +56,7 @@ pipeline: input: - clicommon - clicommon/cli-prenamer + - clicommon/cli-split-operation - clicommon/cli-flatten-setter #- clicommon/cli-poly-as-param-modifier - clicommon/cli-poly-as-resource-modifier @@ -65,6 +70,7 @@ scope-clicommon: output-artifact: - clicommon-output - clicommon-prenamer + - clicommon-split-operation - clicommon-flatten-setter - clicommon-poly-as-resource-modifier #- clicommon-poly-as-param-modifier @@ -95,7 +101,7 @@ modelerfour: LRO: LRO cli: - #flatten: + # flatten: # cli-flatten-set-enabled: true # cli-flatten-all: true # cli-flatten-payload: true @@ -120,6 +126,27 @@ cli: # cli-flatten-payload-max-poly-as-resource-prop-count: 8 # # max properties allowed from flatten of sub-class as param # cli-flatten-payload-max-poly-as-param-prop-count: 8 + + # example for split-operation + # cli-directive: + # - where: + # group: OperationGroupName + # op: CreateOrUpdate + # split-operation-names: + # - Create + # - Update + + split-operation: + # if true, operation with 'split-operation-names' will be splited into multiple + # operations with given names. + # Notice: + # 1. Splitted operation's key is in formate: #. + # For example, in above case, the splitted operation keys are 'CreateOrUpdate#Create' + # and 'CreateOrUpdate#Update'. To make direcitve works on splitted operation, please + # use the new key. + # 2. If operation with split name has already existed in operation group, you will get + # a warning and this split name will be skipped. + cli-split-operation-enabled: true polymorphism: # if true, polymorphism parameter with 'poly-resource' marked as true will be # expanded into multiple operations for each subclasses diff --git a/doc/cli-directive.md b/doc/cli-directive.md index b457213..e9af9ee 100644 --- a/doc/cli-directive.md +++ b/doc/cli-directive.md @@ -79,6 +79,12 @@ For groupName, operationName, parameterName, typeName, propertyName, usually you - old: 'old_value' - new: 'new_value' - isRegex: true | false +- split-operation-names + - split operation into multiple operations with given names + - value format: + - opName1 + - opName2 + - ... ## How to troubleshooting > Add --debug in your command line to have more intermedia output files for troubleshooting @@ -190,5 +196,13 @@ cli: enum: 'enumTyp' value: 'enumValue' alias: NewAlias + # split operation into multiple operations + - where: + group: OperationGroupName + op: CreateOrUpdate + split-operation-names: + - Create + - Update + ``` diff --git a/src/copyHelper.ts b/src/copyHelper.ts new file mode 100644 index 0000000..a22df01 --- /dev/null +++ b/src/copyHelper.ts @@ -0,0 +1,61 @@ +import { Request, Parameter, Operation, Schema } from "@azure-tools/codemodel"; +import { isNullOrUndefined } from "util"; + +export class CopyHelper { + + public static copyOperation(source: Operation, globalParameters?: Parameter[], customizedReqCopy?: (req: Request) => Request, customizedParamCopy?: (srcParam: Parameter) => Parameter): Operation { + const copy = new Operation(source.language.default.name, '', source); + copy.language = CopyHelper.deepCopy(source.language); + copy.extensions = CopyHelper.copy(source.extensions); + copy.parameters = source.parameters?.map((op) => { + if (globalParameters?.find((gp) => gp === op)) { + return op; + } else if (customizedParamCopy) { + return customizedParamCopy(op); + } else { + return CopyHelper.copyParameter(op) + } + }); + copy.requests = source.requests?.map((req) => customizedReqCopy == null ? CopyHelper.copyRequest(req) : customizedReqCopy(req)); + copy.updateSignatureParameters(); + return copy; + } + + public static copyRequest(source: Request, customizedParamCopy?: (param: Parameter) => Parameter): Request { + const copy = new Request(source); + copy.extensions = CopyHelper.copy(source.extensions); + copy.language = CopyHelper.deepCopy(source.language); + copy.parameters = copy.parameters?.map((p) => customizedParamCopy == null ? CopyHelper.copyParameter(p) : customizedParamCopy(p)); + copy.updateSignatureParameters(); + return copy; + } + + public static copyParameter(source: Parameter, customizedSchema?: Schema): Parameter { + const copy = new Parameter(source.language.default.name, source.language.default.description, customizedSchema ?? source.schema, { + implementation: source.implementation, + extensions: {}, + language: CopyHelper.deepCopy(source.language), + protocol: source.protocol, + }); + for (const property in source) { + if (isNullOrUndefined(copy[property])) { + copy[property] = source[property]; + } + } + return copy; + } + + public static copy(source: T): T { + if (source == null) { + return source; + } + return Object.assign({}, source); + } + + public static deepCopy(source: T): T { + if (source == null) { + return source; + } + return JSON.parse(JSON.stringify(source)); + } +} diff --git a/src/helper.ts b/src/helper.ts index 9479806..031153b 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -346,6 +346,7 @@ export class Helper { `${tab(2)}- operationName: ${generateCliValue(vv, 3)}` + (isNullOrUndefined(NodeHelper.getPolyAsResourceParam(vv)) ? '' : `${NEW_LINE}${tab(3)}cli-poly-as-resource-subclass-param: ${NodeHelper.getCliKey(NodeHelper.getPolyAsResourceParam(vv), '')}`) + (isNullOrUndefined(NodeHelper.getPolyAsResourceOriginalOperation(vv)) ? '' : `${NEW_LINE}${tab(3)}cli-poly-as-resource-original-operation: ${NodeHelper.getCliKey(NodeHelper.getPolyAsResourceOriginalOperation(vv), '')}`) + + (isNullOrUndefined(NodeHelper.getSplitOperationOriginalOperation(vv)) ? '' : `${NEW_LINE}${tab(3)}cli-split-operation-original-operation: ${NodeHelper.getCliKey(NodeHelper.getSplitOperationOriginalOperation(vv), '')}`) + `${NEW_LINE}${tab(3)}parameters:${NEW_LINE}`.concat( vv.parameters.map(vvv => `${tab(3)}- parameterName: ${generateCliValue(vvv, 4)}${generatePropertyFlattenValue(vvv, 4)}${generatePropertyReadonlyValue(vvv, 4)}${generateDiscriminatorValueForParam(vvv, 4)}${NEW_LINE}` + (isNullOrUndefined(NodeHelper.getPolyAsResourceBaseSchema(vvv)) ? '' : `${tab(4)}cli-poly-as-resource-base-schema: ${NodeHelper.getCliKey(NodeHelper.getPolyAsResourceBaseSchema(vvv), '')}${NEW_LINE}`) + diff --git a/src/index.ts b/src/index.ts index 0217c41..a18489f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { Helper } from './helper'; import { Modifier } from './plugins/modifier/modifier'; import { CommonNamer } from './plugins/namer'; import { processRequest as flattenSetter } from './plugins/flattenSetter/flattenSetter'; +import { processRequest as splitOperation } from './plugins/splitOperation'; import { processRequest as preNamer } from './plugins/prenamer'; import { CliConst } from './schema'; import { processRequest as polyAsResourceModifier } from './plugins/polyAsResourceModifier'; @@ -42,6 +43,7 @@ extension.Add("clicommon", async host => { extension.Add("cli-flatten-setter", flattenSetter); extension.Add("cli-prenamer", preNamer); +extension.Add("cli-split-operation", splitOperation); extension.Add("cli-poly-as-resource-modifier", polyAsResourceModifier); extension.Add("cli-poly-as-param-modifier", polyAsParamModifier); extension.Add("cli-complex-marker", complexMarker); diff --git a/src/nodeHelper.ts b/src/nodeHelper.ts index 96e0dc3..d93a67b 100644 --- a/src/nodeHelper.ts +++ b/src/nodeHelper.ts @@ -16,6 +16,7 @@ export class NodeHelper { private static readonly CLI_MARK: string = "cli-mark"; private static readonly CLI_IS_VISIBLE: string = "cli-is-visible"; private static readonly CLI_OPERATIONS: string = "cli-operations"; + private static readonly CLI_OPERATION_SPLITTED = 'cli-operation-splitted'; private static readonly JSON: string = "json"; public static readonly FLATTEN_FLAG: string = 'x-ms-client-flatten'; public static readonly DISCRIMINATOR_FLAG: string = 'discriminator'; @@ -27,6 +28,8 @@ export class NodeHelper { private static readonly POLY_AS_PARAM_BASE_SCHEMA = 'cli-poly-as-param-base-schema'; private static readonly POLY_AS_PARAM_ORIGINIAL_PARAMETER = 'cli-poly-as-param-original-parameter'; private static readonly POLY_AS_PARAM_EXPANDED = 'cli-poly-as-param-expanded'; + private static readonly SPLIT_OPERATION_ORIGINAL_OPERATION = 'cli-split-operation-original-operation'; + private static readonly SPLIT_OPERATION_NAMES = 'split-operation-names'; private static visitedKeyDict = {}; @@ -160,6 +163,22 @@ export class NodeHelper { return isNullOrUndefined(node.language[NodeHelper.CLI]) ? '' : node.language[NodeHelper.CLI][NodeHelper.DESCRIPTION]; } + public static setCliOperationSplitted(op: Operation, value: boolean) { + NodeHelper.setCliProperty(op, NodeHelper.CLI_OPERATION_SPLITTED, value); + } + + public static getCliOperationSplitted(op: Operation): boolean { + return NodeHelper.getCliProperty(op, NodeHelper.CLI_OPERATION_SPLITTED, () => null); + } + + public static getCliSplitOperationNames(node: Operation): string[] { + return NodeHelper.getCliProperty(node, this.SPLIT_OPERATION_NAMES, () => null); + } + + public static clearCliSplitOperationNames(node: Operation) { + NodeHelper.clearCliProperty(node, this.SPLIT_OPERATION_NAMES); + } + public static setPolyAsResource(node: Parameter, value: boolean) { NodeHelper.setCliProperty(node, this.POLY_RESOURCE, value); } @@ -192,6 +211,14 @@ export class NodeHelper { return NodeHelper.getExtensionsProperty(op, NodeHelper.POLY_AS_RESOURCE_ORIGINAL_OPERATION, null); } + public static setSplitOperationOriginalOperation(op: Operation, ori: Operation) { + NodeHelper.setExtensionsProperty(op, NodeHelper.SPLIT_OPERATION_ORIGINAL_OPERATION, ori); + } + + public static getSplitOperationOriginalOperation(op: Operation): Schema { + return NodeHelper.getExtensionsProperty(op, NodeHelper.SPLIT_OPERATION_ORIGINAL_OPERATION, null); + } + public static setPolyAsParamBaseSchema(param: Parameter, base: Schema) { NodeHelper.setExtensionsProperty(param, NodeHelper.POLY_AS_PARAM_BASE_SCHEMA, base); } diff --git a/src/plugins/modifier/cliDirectiveAction.ts b/src/plugins/modifier/cliDirectiveAction.ts index 4f68637..87d5219 100644 --- a/src/plugins/modifier/cliDirectiveAction.ts +++ b/src/plugins/modifier/cliDirectiveAction.ts @@ -34,6 +34,9 @@ export abstract class Action { case 'poly-resource': arr.push(new ActionSetProperty(value, key, () => true)); break; + case 'split-operation-names': + arr.push(new ActionSetProperty(value, key, () => null)); + break; case 'delete': arr.push(new ActionDelete(value)); break; @@ -183,4 +186,4 @@ export class ActionReplace extends Action { NodeHelper.setCliProperty(node, this.actionReplace.field, original.replace(regex, this.actionReplace.new)); } } -} \ No newline at end of file +} diff --git a/src/plugins/polyAsResourceModifier.ts b/src/plugins/polyAsResourceModifier.ts index 4b00a5b..f1a440d 100644 --- a/src/plugins/polyAsResourceModifier.ts +++ b/src/plugins/polyAsResourceModifier.ts @@ -1,12 +1,10 @@ -import { Host, Session, startSession } from "@azure-tools/autorest-extension-base"; -import { CodeModel, Request, codeModelSchema, Metadata, ObjectSchema, isObjectSchema, Property, Extensions, Scheme, ComplexSchema, Operation, OperationGroup, Parameter, VirtualParameter, ImplementationLocation } from "@azure-tools/codemodel"; -import { isNullOrUndefined, isArray, isNull } from "util"; +import { Host, Session } from "@azure-tools/autorest-extension-base"; +import { CodeModel, Request, ObjectSchema, Operation, OperationGroup, Parameter } from "@azure-tools/codemodel"; +import { isNullOrUndefined } from "util"; import { Helper } from "../helper"; -import { CliConst, M4Node } from "../schema"; -import { Dumper } from "../dumper"; -import { Dictionary, values } from '@azure-tools/linq'; import { NodeHelper } from "../nodeHelper"; import { FlattenHelper } from "../flattenHelper"; +import { CopyHelper } from "../copyHelper"; export class PolyAsResourceModifier { @@ -22,81 +20,31 @@ export class PolyAsResourceModifier { return (NodeHelper.isPolyAsResource(param)); } - /** - * a simple object clone by using Json serialize and parse - * @param obj - */ - private cloneObject(obj: T): T { - return JSON.parse(JSON.stringify(obj)) as T; - } - - private cloneObjectTopLevel(obj: any) { - let r = {}; - for (let key in obj) { - r[key] = obj[key]; - } - return r; - } - private cloneOperationForSubclass(op: Operation, newDefaultName: string, newCliKey: string, newCliName: string, baseSchema: ObjectSchema, subSchema: ObjectSchema) { let polyParam: Parameter = null; - let cloneParam = (p: Parameter): Parameter => { - - const vp = new Parameter(p.language.default.name, p.language.default.description, p.schema === baseSchema ? subSchema : p.schema, { - implementation: p.implementation, - extensions: {}, - language: this.cloneObject(p.language), - protocol: p.protocol, - }); - - for (let key in p) - if (isNullOrUndefined(vp[key])) - vp[key] = p[key]; - + const cloneParam = (p: Parameter): Parameter => { + const vp = CopyHelper.copyParameter(p, p.schema === baseSchema ? subSchema : p.schema); if (p.schema === baseSchema) { - if (polyParam !== null) + if (polyParam !== null) { throw Error(`Mulitple poly as resource Parameter found: 1) ${polyParam.language.default.name}, 2) ${p.language.default.name}`); - else { + } else { polyParam = vp; NodeHelper.setPolyAsResourceBaseSchema(vp, baseSchema); } } - return vp; }; - let cloneRequest = (req: Request): Request => { - let rr = new Request(req); - rr.extensions = this.cloneObjectTopLevel(rr.extensions); - rr.language = this.cloneObject(rr.language); - rr.parameters = rr.parameters.map(p => cloneParam(p)); - rr.updateSignatureParameters(); - return rr; - } - - let op2 = new Operation( - newDefaultName, - '', - op - ); - op2.language = this.cloneObject(op2.language); + const cloneRequest = (req: Request): Request => CopyHelper.copyRequest(req, cloneParam); + + const op2 = CopyHelper.copyOperation(op, this.session.model.globalParameters, cloneRequest, cloneParam); op2.language.default.name = newDefaultName; NodeHelper.setCliName(op2, newCliName); NodeHelper.setCliKey(op2, newCliKey); - op2.extensions = this.cloneObjectTopLevel(op2.extensions); - op2.parameters = op2.parameters.map(p => { - if (this.session.model.findGlobalParameter(pp => pp === p)) - return p; - else - return cloneParam(p) - }); - op2.requests = op2.requests.map(r => cloneRequest(r)); - op2.updateSignatureParameters(); NodeHelper.setPolyAsResourceParam(op2, polyParam); NodeHelper.setPolyAsResourceOriginalOperation(op2, op); - // Do we need to deep copy response? seems no need return op2; } diff --git a/src/plugins/splitOperation.ts b/src/plugins/splitOperation.ts new file mode 100644 index 0000000..284930d --- /dev/null +++ b/src/plugins/splitOperation.ts @@ -0,0 +1,100 @@ +import { Host, Session } from "@azure-tools/autorest-extension-base"; +import { CodeModel, Request, Operation, Parameter } from "@azure-tools/codemodel"; +import { isNullOrUndefined } from "util"; +import { Helper } from "../helper"; +import { CliConst, CliCommonSchema } from "../schema"; +import { NodeHelper } from "../nodeHelper"; +import { Modifier } from "./modifier/modifier"; +import { CopyHelper } from "../copyHelper"; + +export class SplitOperation{ + + constructor(protected session: Session){ + } + + public async process() { + + await this.modifier(); + + for (const group of this.session.model.operationGroups) { + // Operation will be splitted with given names. To avoid duplicated operation name error in modelerfour, we compare + // split names with existed names. If it has already existed, skip this split name. + const existedNames = new Set(group.operations.map((op) => op.language.default.name.toUpperCase())); + const splittedGroupOperations = []; + for (const operation of group.operations) { + const splitNames = NodeHelper.getCliSplitOperationNames(operation); + if (!splitNames || splitNames.length === 0) { + continue; + } + const splittedOperations = this.splitOperations(splitNames, operation, existedNames); + + splittedOperations.forEach((splittedOperation) => { + // Link src operation to splitted operation + NodeHelper.addCliOperation(operation, splittedOperation); + // Link splitted operation to src opreation + NodeHelper.setSplitOperationOriginalOperation(splittedOperation, operation); + + splittedGroupOperations.push(splittedOperation); + }); + + if (splittedOperations.length > 0) { + NodeHelper.setCliOperationSplitted(operation, true); + } + } + splittedGroupOperations.forEach((op) => group.addOperation(op)); + } + } + + private async modifier() { + const directives = (await this.session.getValue(CliConst.CLI_DIRECTIVE_KEY, [])).filter((dir) => dir[CliConst.CLI_SPLIT_OPERATION_NAMES_KEY]); + if (directives && directives.length > 0) { + Helper.dumper.dumpCodeModel('split-operation-modifier-pre'); + const modifier = await new Modifier(this.session).init(directives); + modifier.process(); + Helper.dumper.dumpCodeModel('split-operation-modifier-post'); + } else { + Helper.logDebug('No split operation directive is found!'); + } + } + + private splitOperations(splitNames: string[], srcOperation: Operation, existedNames: Set): Operation[] { + const splittedOperations = []; + for (const splitName of splitNames) { + if (existedNames.has(splitName.toUpperCase())) { + Helper.logWarning(`Operation ${splitName} has already existed in group! Skip split!`); + continue; + } + const splittedOperation = this.splitOperation(splitName, srcOperation); + splittedOperations.push(splittedOperation); + } + return splittedOperations; + } + + private splitOperation(splitName: string, srcOperation: Operation): Operation { + const operation = CopyHelper.copyOperation(srcOperation, this.session.model.globalParameters); + operation.language.default.name = splitName; + // Splited operation's cli key in format: # + NodeHelper.setCliKey(operation, `${NodeHelper.getCliKey(srcOperation, srcOperation.language.default.name)}#${splitName}`); + NodeHelper.clearCliSplitOperationNames(operation); + return operation; + } +} + +export async function processRequest(host: Host) { + + const session = await Helper.init(host); + Helper.dumper.dumpCodeModel("split-operation-pre"); + + const expandEnabled = (await session.getValue(CliConst.CLI_SPLIT_OPERATION_ENABLED_KEY, false)) === true; + if (!expandEnabled) { + Helper.logDebug(`cli-split-operation-enabled is not true. Skip split operation`); + } else { + const splitOperation = new SplitOperation(session); + await splitOperation.process(); + } + + Helper.dumper.dumpCodeModel("split-operation-post"); + + Helper.outputToModelerfour(); + await Helper.dumper.persistAsync(); +} diff --git a/src/schema.ts b/src/schema.ts index f89e975..4e269cb 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -26,6 +26,9 @@ export namespace CliConst { export const CLI_FLATTEN_SET_FLATTEN_PAYLOAD_MAX_POLY_AS_PARAM_PROP_KEY: string = 'cli.flatten.cli-flatten-payload-max-poly-as-param-prop-count'; export const CLI_FLATTEN_SET_FLATTEN_ALL_OVERWRITE_SWAGGER_KEY: string = 'cli.flatten.cli-flatten-all-overwrite-swagger'; + export const CLI_SPLIT_OPERATION_ENABLED_KEY: string = 'cli.split-operation.cli-split-operation-enabled'; + export const CLI_SPLIT_OPERATION_NAMES_KEY: string = 'split-operation-names'; + export const DEFAULT_OPERATION_PARAMETER_INDEX = -1; export class NamingStyle {