diff --git a/doc/cli-directive.md b/doc/cli-directive.md index c221087..98805cf 100644 --- a/doc/cli-directive.md +++ b/doc/cli-directive.md @@ -34,6 +34,9 @@ For groupName, operationName, parameterName, typeName, propertyName, usually you - search for enum or enumValue - 'choiceSchema' | 'enum': 'choiceName' - 'choiceValue' | 'value': 'choiceName' + - search for example and path in it + - 'exampleName' + - 'path' | 'dotPath' | 'exampleParameterPath': 'dotPath' - set: - set anything property in the selected object(s) - optional @@ -91,6 +94,10 @@ For groupName, operationName, parameterName, typeName, propertyName, usually you - add 'min-api: ..." under 'language->cli' - max-api: - add 'max-api: ..." under 'language->cli' +- value: + - set static value for example dotPath +- eval: + - set dynamic value for example dotPath with an evaluatable script ## How to troubleshooting > Add --debug in your command line to have more intermedia output files for troubleshooting @@ -215,6 +222,16 @@ cli: op: CreateOrUpdate#Update param: properties cli-flatten: true + # set example content with static value + - where: + exampleName: TheExampleName + exampleParameterPath: parameters.hostPool.location + value: Shanghai + # set example content with dynamic script + - where: + exampleName: TheExampleName + exampleParameterPath: parameters.registration-info.expiration-time + eval: "var d = new Date(); d.setDate(d.getDate()+15); var ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(d); var mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(d); var da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d); `${ye}-${mo}-${da}T00:00:00.000Z` " ``` diff --git a/src/helper.ts b/src/helper.ts index b8a4439..38a0011 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -132,6 +132,8 @@ export class Helper { public static ToM4NodeType(node: M4Node): M4NodeType { if (Helper.isOperationGroup(node)) return CliConst.SelectType.operationGroup; + else if (Helper.isExample(node)) + return CliConst.SelectType.dotPath; else if (Helper.isOperation(node)) return CliConst.SelectType.operation; else if (Helper.isParameter(node)) @@ -568,6 +570,12 @@ export class Helper { Helper.enumerateRequestParameters(group, op, paths, action, flag); paths.pop(); + paths.push('extensions'); + paths.push('x-ms-examples'); + Helper.enumerateExamples(group, op, paths, action, flag); + paths.pop(); + paths.pop(); + paths.pop(); } } @@ -631,6 +639,26 @@ export class Helper { } } + public static enumerateExamples(group: OperationGroup, op: Operation, paths: string[], action: (nodeDescriptor: CliCommonSchema.CodeModel.NodeDescriptor) => void, flag: CliCommonSchema.CodeModel.NodeTypeFlag): void { + const enumExample = isNullOrUndefined(flag) || ((flag & CliCommonSchema.CodeModel.NodeTypeFlag.dotPath) > 0); + if (!enumExample || isNullOrUndefined(op.extensions?.['x-ms-examples'])) return; + const cliKeyMissing = ''; + + for (let exampleName of Object.getOwnPropertyNames(op.extensions['x-ms-examples'])) { + const example = op.extensions['x-ms-examples'][exampleName]; + paths.push(`['${exampleName}']`); + action({ + operationGroupCliKey: NodeCliHelper.getCliKey(group, cliKeyMissing), + operationCliKey: NodeCliHelper.getCliKey(op, cliKeyMissing), + parent: op.extensions['x-ms-examples'], + target: example, + targetIndex: -1, + exampleName: exampleName, + }); + paths.pop(); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types public static isOperationGroup(o: any): boolean { if (isNullOrUndefined(o)) { @@ -822,6 +850,20 @@ export class Helper { return false; } + public static isExample(o: any): boolean { + if (isNullOrUndefined(o)) { + return false; + } + if (o.__proto__ !== Object.prototype) { + return false; + } + const props = Object.getOwnPropertyNames(o); + if (props.find((prop) => prop === 'parameters') && props.find((prop) => prop === 'responses') && props.length==2) { + return true; + } + return false; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types public static isSchema(o: any): boolean { if (isNullOrUndefined(o)) { @@ -873,4 +915,20 @@ export class Helper { return m4Path; } + public static setPathValue (obj, path, value) { + if (Object(obj) !== obj) return obj; // When obj is not an object + // If not yet an array, get the keys from the string-path + if (!Array.isArray(path)) path = path.toString().match(/[^.[\]]+/g) || []; + path.slice(0,-1).reduce((a, c, i) => // Iterate all of them except the last one + Object(a[c]) === a[c] // Does the key exist and is its value an object? + // Yes: then follow that path + ? a[c] + // No: create the key. Is the next key a potential array-index? + : a[c] = Math.abs(path[i+1])>>0 === +path[i+1] + ? [] // Yes: assign a new array object + : {}, // No: assign a new plain object + obj)[path[path.length-1]] = value; // Finally assign the value to the last key + return obj; // Return the top-level object to allow chaining + }; + } \ No newline at end of file diff --git a/src/plugins/m4namer.ts b/src/plugins/m4namer.ts index 513a076..92832ae 100644 --- a/src/plugins/m4namer.ts +++ b/src/plugins/m4namer.ts @@ -84,7 +84,7 @@ export class M4CommonNamer { private retrieveCodeModel(model: CodeModel): void { Helper.enumerateCodeModel(model, (n) => { - if (!isNullOrUndefined(n.target.language['cli'])) { + if (!isNullOrUndefined(n.target.language?.['cli'])) { // log path for code model NodeCliHelper.setCliPath(n.target, n.nodePath); } diff --git a/src/plugins/modelerPostProcessor.ts b/src/plugins/modelerPostProcessor.ts index 90325bc..85971a3 100644 --- a/src/plugins/modelerPostProcessor.ts +++ b/src/plugins/modelerPostProcessor.ts @@ -21,7 +21,7 @@ export class ModelerPostProcessor { private retrieveCodeModel(model: CodeModel): void { Helper.enumerateCodeModel(model, (n) => { - if (!isNullOrUndefined(n.target.language['cli'])) { + if (!isNullOrUndefined(n.target.language?.['cli'])) { // In case cli is shared by multiple instances during modelerfour, do deep copy n.target.language['cli'] = CopyHelper.deepCopy(n.target.language['cli']); diff --git a/src/plugins/modifier/cliDirectiveAction.ts b/src/plugins/modifier/cliDirectiveAction.ts index f0385c2..2a53a51 100644 --- a/src/plugins/modifier/cliDirectiveAction.ts +++ b/src/plugins/modifier/cliDirectiveAction.ts @@ -1,6 +1,7 @@ import { isNullOrUndefined, isArray } from "util"; import { CliCommonSchema, CliConst } from "../../schema"; import { NodeHelper, NodeCliHelper, NodeExtensionHelper } from "../../nodeHelper"; +import { Helper } from "../../helper"; function validateDirective(directive: CliCommonSchema.CliDirective.Directive | string, name: string): void { @@ -70,6 +71,12 @@ export abstract class Action { case 'hitcount': arr.push(new ActionHitCount(value)); break; + case 'value': + arr.push(new ActionSetValue(value, directive.where.dotPath)); + break; + case 'eval': + arr.push(new ActionSetValue(eval(value), directive.where.dotPath)); + break; default: // TODO: better to log instead of throw here? throw Error(`Unknown directive operation: '${key}'`); @@ -214,3 +221,18 @@ export class ActionReplace extends Action { } } } + + +export class ActionSetValue extends Action { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(private directiveValue: CliCommonSchema.CliDirective.ValueClause, private dotPath: string) { + super(); + } + + public process(descriptor: CliCommonSchema.CodeModel.NodeDescriptor): void { + if (!isNullOrUndefined(this.dotPath)) { + Helper.setPathValue(descriptor.target, this.dotPath, this.directiveValue); + } + } +} diff --git a/src/plugins/modifier/cliDirectiveSelector.ts b/src/plugins/modifier/cliDirectiveSelector.ts index 8d4ecec..497201a 100644 --- a/src/plugins/modifier/cliDirectiveSelector.ts +++ b/src/plugins/modifier/cliDirectiveSelector.ts @@ -20,6 +20,8 @@ export class NodeSelector { property: ['prop'], choiceSchema: ['enum', 'choice-schema'], choiceValue: ['value', 'choice-value'], + exampleName: ['example-name'], + dotPath: ['path', 'dot-path', 'example-parameter-path'], }; for (const key in alias) { @@ -43,6 +45,8 @@ export class NodeSelector { this.selectType = CliConst.SelectType.choiceValue; else if (!Helper.isEmptyString(this.where.choiceSchema)) this.selectType = CliConst.SelectType.choiceSchema; + else if (!Helper.isEmptyString(this.where.dotPath)) + this.selectType = CliConst.SelectType.dotPath; else throw Error("SelectType missing in directive: " + JSON.stringify(this.where)); } @@ -75,6 +79,11 @@ export class NodeSelector { r = match(this.where.objectSchema, descriptor.objectSchemaCliKey) && match(this.where.property, descriptor.propertyCliKey); break; + case CliConst.SelectType.dotPath: + r = match(this.where.operationGroup, descriptor.operationGroupCliKey) && + match(this.where.operation, descriptor.operationCliKey) && + match(this.where.exampleName, descriptor.exampleName); + break; default: throw Error(`Unknown select type: ${this.selectType}`); } diff --git a/src/plugins/namer.ts b/src/plugins/namer.ts index 6538183..325e383 100644 --- a/src/plugins/namer.ts +++ b/src/plugins/namer.ts @@ -87,7 +87,7 @@ export class CommonNamer { private retrieveCodeModel(model: CodeModel): void { Helper.enumerateCodeModel(model, (n) => { - if (!isNullOrUndefined(n.target.language['cli'])) { + if (!isNullOrUndefined(n.target?.language?.['cli'])) { // log path for code model NodeCliHelper.setCliPath(n.target, n.nodePath); } diff --git a/src/plugins/prenamer.ts b/src/plugins/prenamer.ts index bb83251..9caf57d 100644 --- a/src/plugins/prenamer.ts +++ b/src/plugins/prenamer.ts @@ -14,7 +14,7 @@ export class PreNamer{ public async process(): Promise { Helper.enumerateCodeModel(this.session.model, (n) => { - if (!isNullOrUndefined(n.target.language.default.name)) + if (!isNullOrUndefined(n.target.language?.default?.name)) NodeCliHelper.setCliKey(n.target, n.target.language.default.name); }); diff --git a/src/schema.ts b/src/schema.ts index 40e9106..851bc29 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -2,7 +2,7 @@ import { ChoiceValue, Metadata } from "@azure-tools/codemodel"; export type NamingStyle = 'camel' | 'pascal' | 'snake' | 'upper' | 'kebab' | 'space'; export type NamingType = 'parameter' | 'operation' | 'operationGroup' | 'property' | 'type' | 'choice' | 'choiceValue' | 'constant' | 'client'; -export type M4NodeType = 'operationGroup' | 'operation' | 'parameter' | 'objectSchema' | 'property' | 'choiceSchema' | 'choiceValue'; +export type M4NodeType = 'operationGroup' | 'operation' | 'parameter' | 'objectSchema' | 'property' | 'choiceSchema' | 'choiceValue' | 'dotPath'; export type LanguageType = 'cli' | 'default'; export type M4Node = Metadata | ChoiceValue; @@ -73,6 +73,7 @@ export namespace CliConst { static readonly property = 'property'; static readonly choiceSchema = 'choiceSchema'; static readonly choiceValue = 'choiceValue'; + static readonly dotPath = 'dotPath'; } } @@ -117,6 +118,8 @@ export namespace CliCommonSchema { property?: string; choiceSchema?: string; choiceValue?: string; + exampleName?: string; + dotPath?: string; } export interface FormatTableClause { @@ -170,7 +173,8 @@ export namespace CliCommonSchema { objectSchema = 8, property = 16, choiceSchema = 32, - choiceValue = 64 + choiceValue = 64, + dotPath = 128, } export enum Complexity { @@ -233,6 +237,7 @@ export namespace CliCommonSchema { * cliKey: nextLink **/ nodePath?: string; + exampleName?: string; } } } diff --git a/test/test_directive_action_setValue.ts b/test/test_directive_action_setValue.ts new file mode 100644 index 0000000..8c6b4f5 --- /dev/null +++ b/test/test_directive_action_setValue.ts @@ -0,0 +1,44 @@ +import { assert } from 'chai'; +import 'mocha'; +import { ActionSet, ActionSetProperty, ActionSetValue } from '../src/plugins/modifier/cliDirectiveAction'; +import { M4Node } from '../src/schema'; +import { Metadata } from "@azure-tools/codemodel"; + +describe('Test Directive - Action - setValue', function () { + var descriptor = { + parent: null, + targetIndex: -1, + target: null, + exampleName: 'Example1' + }; + + it('directive setProperty - string', () => { + let baseline = "someValue"; + let action = new ActionSetValue(baseline, "a.b.c"); + + descriptor.target = new Metadata(); + action.process(descriptor); + assert.deepEqual(descriptor.target.a.b.c, baseline); + }); + + it('directive setProperty - number', () => { + let baseline = 123; + let action = new ActionSetValue(baseline, "a.b.c"); + + descriptor.target = new Metadata(); + action.process(descriptor); + assert.deepEqual(descriptor.target.a.b.c, baseline); + }); + + it('directive setProperty - object', () => { + let baseline = { + key: "someValue", + }; + let action = new ActionSetValue(baseline, "a.b.c"); + + descriptor.target = new Metadata(); + action.process(descriptor); + assert.deepEqual(descriptor.target.a.b.c, baseline); + }); + +}); \ No newline at end of file diff --git a/test/test_directive_selector_dotpath.ts b/test/test_directive_selector_dotpath.ts new file mode 100644 index 0000000..66093d2 --- /dev/null +++ b/test/test_directive_selector_dotpath.ts @@ -0,0 +1,197 @@ +import { Parameter, Value } from "@azure-tools/codemodel"; +import { expect, assert } from 'chai'; +import 'mocha'; +import { NodeSelector } from '../src/plugins/modifier/cliDirectiveSelector'; +import "@azure-tools/codemodel"; + +function mockExample(): any{ + return { + parameters: null, + responses: null, + } +} + +describe('Test Directive - Selector - dotPath', function () { + it('select dotPath - normal', () => { + + let selector = new NodeSelector({ + select: 'dotPath', + where: { + operationGroup: 'og1', + operation: 'o1', + exampleName: 'Example1', + dotPath: 'a.b.c', + } + }); + + assert.isTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example2", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og2', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o2', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + }); + + it('select dotPath - without exampleName', () => { + let selector = new NodeSelector({ + select: 'dotPath', + where: { + operationGroup: 'og1', + operation: 'o1', + dotPath: 'a.b.c', + } + }); + + assert.isTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example2", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og2', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o2', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + }); + + it('select parameter - without operationGroup', () => { + + let selector = new NodeSelector({ + select: 'dotPath', + where: { + operation: 'o1', + exampleName: 'Example1', + dotPath: 'a.b.c', + } + }); + + assert.isTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isTrue(selector.match({ + operationGroupCliKey: 'og2', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og2', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example2", + })); + }); + + it('select parameter - without operation', () => { + + let selector = new NodeSelector({ + select: 'dotPath', + where: { + operationGroup: 'og1', + exampleName: 'Example1', + dotPath: 'a.b.c', + } + }); + + assert.isTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isTrue(selector.match({ + operationGroupCliKey: 'og1', + operationCliKey: 'o2', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og2', + operationCliKey: 'o1', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example2", + })); + + assert.isNotTrue(selector.match({ + operationGroupCliKey: 'og2', + operationCliKey: 'o2', + parent: null, + targetIndex: -1, + target: mockExample() , + exampleName: "Example1", + })); + }); +}); \ No newline at end of file