diff --git a/README.md b/README.md index 497159b..ebe324f 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,34 @@ modelerfour: client: 'pascal' clicommon: - naming: - singularize: - - operationGroup - - operation - parameter: 'camel' - operation: 'pascal' - operationGroup: 'pascal' - property: 'camel' - type: 'pascal' - choice: 'pascal' - choiceValue: 'pascal' + naming: + cli: + singularize: + - operationGroup + - operation + override: + cmyk : CMYK + $host: $host + LRO: LRO + parameter: 'camel' + operation: 'pascal' + operationGroup: 'pascal' + property: 'camel' + type: 'pascal' + choice: 'pascal' + choiceValue: 'pascal' + default: + override: + cmyk : CMYK + $host: $host + LRO: LRO + parameter: 'camel' + operation: 'pascal' + operationGroup: 'pascal' + property: 'camel' + type: 'pascal' + choice: 'pascal' + choiceValue: 'pascal' ``` # Available Configurations for CLI Common: @@ -62,29 +79,42 @@ clicommon: > Naming convention to be used in the output of clicommon. > Please make sure **snake_naming_convention** is used if the name is changed through directive -> so that it will be converted to correct naming convention in the output +> so that it will be converted to correct naming convention in the output. Samples as below: ``` $(sample-cli-directive) clicommon: - # known word that won't be singularized - glossary: - - 'insights' - naming: - # list the resource that needs to singularize - singularize: - - operationGroup - - operation - # possible value: pascal|camel|snake|upper|kebab|space - parameter: 'camel' - operation: 'pascal' - operationGroup: 'pascal' - property: 'camel' - type: 'pascal' - choice: 'pascal' - choiceValue: 'pascal' + naming: + cli: + singularize: + - operationGroup + - operation + override: + cmyk : CMYK + $host: $host + LRO: LRO + parameter: 'camel' + operation: 'pascal' + operationGroup: 'pascal' + property: 'camel' + type: 'pascal' + choice: 'pascal' + choiceValue: 'pascal' + default: + override: + cmyk : CMYK + $host: $host + LRO: LRO + parameter: 'camel' + operation: 'pascal' + operationGroup: 'pascal' + property: 'camel' + type: 'pascal' + choice: 'pascal' + choiceValue: 'pascal' ``` + ## Directive > so please make sure **snake_naming_convention** is used for 'where' and 'name' clause in directive @@ -100,6 +130,7 @@ clicommon: - conditions to locate the object to apply directive - required - **snake_naming_converion** is expected as the value + - regex is supported in the value - possible search condition, refer to sample below for more detail usage: - search for operatoinGroup, operation or parameter - 'operationGroup' | 'group' | 'resource': 'name_in_snake_naming_convention' @@ -115,22 +146,33 @@ clicommon: - set anything property in the selected object(s) - optional - name: - - add 'name: ...' in the selected object(s). Please make sure **snake_naming_convention** is used + - add 'name: ...' under 'language->cli'. Please make sure **snake_naming_convention** is used - optional - hide: - - add 'hide: ...' in the selected object(s). + - add 'hide: ...' under 'language->cli'. - optional - remove: - - add 'remove: ...' in the seleected object(s). + - add 'remove: ...' under 'language->cli'. + - optional +- alias: + - add 'alias: ...' under 'language->cli' - optional - formatTable: - - add properties information in the selected object(s). + - add properties information under 'language->cli'. - optional - - possible sub item: + - value format: - properties: - prop1Name - prop2Name - ... +- replace: + - do replacement + - optional + - value format: + - field: 'name' + - old: 'old_value' + - new: 'new_value' + - isRegex: true | false #### How to figure out the name to be used in directives - Enable output for clicommon by following configuration: @@ -145,20 +187,18 @@ clicommon: ``` $(sample-cli-directive) clicommon: cli-directive: - # set operationGroup's name. - # as alias, 'resource' or 'group' can also be used + # directive on operationGroup - select: 'operationGroup' where: operationGroup: 'old_name' name: 'new_name' - where: resource: 'old_name' - name: 'new_name' + hide: true - where: group: 'old_name' - name: 'new_name' + remove: 'true # add hide property for operation - # as alias, 'op' can also be used - where: group: 'group_name' operation: 'operation_name' @@ -168,7 +208,6 @@ clicommon: op: 'operatoin_name' hide: true # add remove property for parameter - # as alias, 'param' can also be used - where: group: 'group_name' op: 'operation_name' @@ -179,8 +218,11 @@ clicommon: op: 'operation_name' param: 'parameter_name' remove: true + # add hide property for all parameter start with 'abc' + - where: + parameter: '^abc.*$' + hide: true # set table format under for schema - # as alias, 'type' and 'object' can also be used - where: schemaObject: 'schema_name' tableFormat: @@ -200,7 +242,6 @@ clicommon: - 'p1' - 'p2' # set anything for schema property - # as alias, 'prop' can also be used - where: type: 'schema_name' property: 'property_name' @@ -219,7 +260,7 @@ clicommon: key3: - v1 - v2 - # replace 'name_a' with 'name_b' (whole word match) + # replac 'name_a' with 'name_b' (whole word match) in operation's name - where: group: 'group_name' op: 'operation_name' @@ -234,10 +275,10 @@ clicommon: op: 'operation_name' replace: field: 'description' - old: 'startByThis(.*)' - new: 'startByThat$1' + old: '(startByThis)(.*)' + new: 'startByThat$2' isRegex: true - # add alias property + # add alias for enum value - where: choiceSchema: 'choice_type' choiceValue: 'choice_value_name' diff --git a/src/helper.ts b/src/helper.ts index 2687a7d..a6dc8fe 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,7 +1,9 @@ import { ChoiceSchema, ChoiceValue, CodeModel, ObjectSchema, Operation, OperationGroup, Parameter, Property, SealedChoiceSchema } from "@azure-tools/codemodel"; import { keys } from "@azure-tools/linq"; import { isArray, isNull, isNullOrUndefined, isObject, isString, isUndefined } from "util"; -import { CliConst, M4Node, M4NodeType, NamingType } from "./schema"; +import { CliConst, M4Node, M4NodeType, NamingType, CliCommonSchema } from "./schema"; +import { pascalCase, EnglishPluralizationService } from '@azure-tools/codegen'; + export class Helper { public static isEmptyString(str): boolean { @@ -101,6 +103,98 @@ export class Helper { throw Error(`Unsupported node type: ${typeof (node)}`); } + public static singularize(settings: CliCommonSchema.NamingConvention, word: string): string { + let low = word.toLowerCase(); + if (settings.glossary.findIndex(v => v === low) >= 0) + return word; + + const eps = new EnglishPluralizationService(); + eps.addWord('Database', 'Databases'); + eps.addWord('database', 'databases'); + return eps.singularize(word); + } + + public static normalizeNamingSettings(settings: CliCommonSchema.NamingConvention) { + if (isNullOrUndefined(settings.singularize)) + settings.singularize = []; + if (isNullOrUndefined(settings.glossary)) + settings.glossary = []; + else + settings.glossary = settings.glossary.map(v => v.toLowerCase()); + if (isNullOrUndefined(settings.override)) + settings.override = {}; + else { + for (let key in settings.override) + settings.override[key.toLowerCase()] = settings.override[key]; + } + return settings; + } + + /** + * Remark: Please make sure the singularize, glossary and override is set to [] or {} instead of null or undefined when calling this method + * No check for them will be done in this method for better performance. exception may be thrown if they are null or undefiend. + * You can call Helper.normalizeNamingSettings to do these normalization + * @param settings + * @param node + * @param languageKey + */ + public static applyNamingConvention(settings: CliCommonSchema.NamingConvention, node: M4Node, languageKey: string) { + if (isNullOrUndefined(node.language[languageKey])) + return; + + let namingType = Helper.ToNamingType(node); + if (isNullOrUndefined(namingType)) { + // unsupported modelerfour node for naming type, ignore it for now + return; + } + + let style = settings[namingType]; + let single = settings.singularize.includes(namingType) === true; + + if (Helper.isEmptyString(style)) { + // Only process when naming convention is set + return; + } + + let oldName: string = node.language[languageKey]['name']; + let up1 = (n: string) => n.length == 1 ? n.toUpperCase() : n[0].toUpperCase().concat(n.substr(1).toLowerCase()); + let op = {}; + op[CliConst.NamingStyle.camel] = { + wording: (v: string, i: number) => i === 0 ? v.toLowerCase() : up1(v), + sep: '', + }; + op[CliConst.NamingStyle.pascal] = { + wording: (v: string, i: number) => up1(v), + sep: '', + }; + op[CliConst.NamingStyle.kebab] = { + wording: (v: string, i: number) => v.toLowerCase(), + sep: '-', + }; + op[CliConst.NamingStyle.snake] = { + wording: (v: string, i: number) => v.toLowerCase(), + sep: '_', + }; + op[CliConst.NamingStyle.space] = { + wording: (v: string, i: number) => v.toLowerCase(), + sep: ' ', + }; + op[CliConst.NamingStyle.upper] = { + wording: (v: string, i: number) => v.toUpperCase(), + sep: '_', + }; + + // the oldName should be in snake_naming_convention + const SEP = '_'; + let newName = oldName.split(SEP).map((v, i) => + (!isNullOrUndefined(settings.override[v.toLowerCase()])) + ? settings.override[v.toLowerCase()] + : op[style].wording(single ? Helper.singularize(settings, v) : v, i) + ).join(op[style].sep); + + node.language[languageKey]['name'] = newName; + } + public static toYamlSimplified(codeModel: CodeModel): string { const INDENT = ' '; const NEW_LINE = '\n'; @@ -131,7 +225,7 @@ export class Helper { v.operations.map(vv => `${tab(2)}- operationName: ${generateCliValue(vv, 3)}` + `${NEW_LINE}${tab(3)}parameters:${NEW_LINE}`.concat( vv.request.parameters.map(vvv => `${tab(3)}- parameterName: ${generateCliValue(vvv, 4)}${NEW_LINE}` + - ((vvv.protocol.http.in === 'body') ? `${tab(4)}bodySchema: ${vvv.schema.language.default.name}${NEW_LINE}` : '')) + (((!isNullOrUndefined(vvv.protocol?.http?.in)) && vvv.protocol.http.in === 'body') ? `${tab(4)}bodySchema: ${vvv.schema.language.default.name}${NEW_LINE}` : '')) .join(''))).join(''))).join('')); s = s + `schemas:${NEW_LINE}` + `${tab()}objects:${NEW_LINE}` + diff --git a/src/plugins/namer.ts b/src/plugins/namer.ts index f108384..94e24d9 100644 --- a/src/plugins/namer.ts +++ b/src/plugins/namer.ts @@ -1,17 +1,16 @@ import { CodeModel, codeModelSchema, Property, Language, Metadata, Operation, OperationGroup, Parameter, ComplexSchema, ObjectSchema, ChoiceSchema, ChoiceValue, SealedChoiceSchema } from '@azure-tools/codemodel'; import { Session, Host, startSession, Channel } from '@azure-tools/autorest-extension-base'; import { serialize, deserialize } from '@azure-tools/codegen'; -import { values, items, length, Dictionary } from '@azure-tools/linq'; +import { values, items, length, Dictionary, keys } from '@azure-tools/linq'; import { isNullOrUndefined } from 'util'; import { CliCommonSchema, CliConst, LanguageType, M4Node } from '../schema'; import { Helper } from '../helper'; -import { pascalCase, EnglishPluralizationService } from '@azure-tools/codegen'; export class CommonNamer { codeModel: CodeModel - namingConvention: CliCommonSchema.NamingConvention + cliNamingSettings: CliCommonSchema.NamingConvention + defaultNamingSettings: CliCommonSchema.NamingConvention flag: Set - glossary: string[] constructor(protected session: Session) { this.codeModel = session.model; @@ -19,10 +18,9 @@ export class CommonNamer { async init() { // any configuration if necessary - this.namingConvention = await this.session.getValue("naming", {}); - if (isNullOrUndefined(this.namingConvention.singularize)) - this.namingConvention.singularize = []; - this.glossary = await this.session.getValue("glossary", []); + this.cliNamingSettings = Helper.normalizeNamingSettings(await this.session.getValue("naming.cli", {})); + this.defaultNamingSettings = Helper.normalizeNamingSettings(await this.session.getValue("naming.default", {})); + return this; } @@ -35,77 +33,7 @@ export class CommonNamer { this.flag = null; return this.codeModel; } - - singularize(word: string): string { - let loWord = word.toLowerCase(); - if (this.glossary.findIndex(v => v === loWord) >= 0) - return word; - const eps = new EnglishPluralizationService(); - eps.addWord('Database', 'Databases'); - eps.addWord('database', 'databases'); - return eps.singularize(word); - } - - /** - * only support Operation, OperationGroup, Parameter, Property, ObjectSchema for now - * @param oldName - * @param metadata - */ - public convertNamingConvention(node: M4Node) { - if (isNullOrUndefined(node.language['cli'])) - return; - - let namingType = Helper.ToNamingType(node); - if (isNullOrUndefined(namingType)) { - // unsupported modelerfour node for naming type, ignore it - return; - } - - let style = this.namingConvention[namingType]; - let single = this.namingConvention.singularize.includes(namingType); - - if (Helper.isEmptyString(style)) { - // Only process when naming convention is set - return; - } - - let oldName: string = node.language['cli']['name']; - let glossary: string[] = node.language['cli']['glossary']; - if (isNullOrUndefined(glossary)) - glossary = []; - - let getSingleArr = (n: string) => n.split(SEP).map(v => this.singularize(v)); - let getPluralArr = (n: string) => n.split(SEP); - let up1 = (n: string) => n.length == 1 ? n.toUpperCase() : n[0].toUpperCase().concat(n.substr(1).toLowerCase()); - - const SEP = '_'; - let newName: string; - switch (style) { - case CliConst.NamingStyle.camel: - newName = (single ? getSingleArr(oldName) : getPluralArr(oldName)).map((v, i) => i === 0 ? v : up1(v)).join(''); - break; - case CliConst.NamingStyle.kebab: - newName = single ? getSingleArr(oldName).join('-') : oldName.replace(SEP, '-'); - break; - case CliConst.NamingStyle.snake: - newName = single ? getSingleArr(oldName).join('_') : oldName; - break; - case CliConst.NamingStyle.pascal: - newName = (single ? getSingleArr(oldName) : getPluralArr(oldName)).map(v => up1(v)).join(''); - break; - case CliConst.NamingStyle.space: - newName = single ? getSingleArr(oldName).join(' ') : oldName.replace(SEP, ' '); - break; - case CliConst.NamingStyle.upper: - newName = single ? getSingleArr(oldName).join('_').toUpperCase() : oldName.toUpperCase(); - break; - default: - throw Error(`Unknown name style: ${style}`) - } - - node.language['cli']['name'] = newName; - } getCliName(obj: any) { if (obj == null || obj.language == null) { @@ -121,7 +49,8 @@ export class CommonNamer { if (!this.flag.has(obj)) { this.flag.add(obj); - this.convertNamingConvention(obj); + Helper.applyNamingConvention(this.cliNamingSettings, obj, 'cli'); + Helper.applyNamingConvention(this.defaultNamingSettings, obj, 'default'); } } diff --git a/src/schema.ts b/src/schema.ts index 3606c97..0074420 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -109,6 +109,8 @@ export namespace CliCommonSchema { export interface NamingConvention { singularize?: NamingType[] + glossary?: string[] + override?: any parameter?: NamingStyle operation?: NamingStyle operationGroup?: NamingStyle diff --git a/test/test_namingConvention.ts b/test/test_namingConvention.ts new file mode 100644 index 0000000..592e2a8 --- /dev/null +++ b/test/test_namingConvention.ts @@ -0,0 +1,61 @@ +import { assert } from 'chai'; +import 'mocha'; +import { ActionSet, ActionFormatTable } from '../src/plugins/modifier/cliDirectiveAction'; +import { M4Node, CliConst, CliCommonSchema } from '../src/schema'; +import { Metadata, OperationGroup, Operation, Parameter, ObjectSchema, Property, ChoiceSchema, ChoiceValue } from "@azure-tools/codemodel"; +import { Helper } from '../src/helper'; + +describe('Test NamingConvention', function () { + it('namingConvention', () => { + + let settings: CliCommonSchema.NamingConvention = { + singularize: ['operation', 'operationGroup'], + glossary: ['cats', 'Dogs'], + override: { + autoRest: 'AUTOREST', + AmE: 'AME', + }, + parameter: 'camel', + operation: 'pascal', + operationGroup: 'upper', + property: 'kebab', + type: 'snake', + choice: 'space', + choiceValue: 'camel' + }; + settings = Helper.normalizeNamingSettings(settings); + + let group: OperationGroup = new OperationGroup('cats_food'); + Helper.applyNamingConvention(settings, group, 'default'); + assert.equal(group.language['default'].name, 'CATS_FOOD'); + + let op: Operation = new Operation('birds_food', 'desc'); + Helper.applyNamingConvention(settings, op, 'default'); + assert.equal(op.language['default'].name, 'BirdFood'); + + let param: Parameter = new Parameter('try_ame_test', 'desc' ,null); + Helper.applyNamingConvention(settings, param, 'default'); + assert.equal(param.language['default'].name, 'tryAMETest') + + let schema: ObjectSchema = new ObjectSchema('AUTOREST_is_cool', 'desc'); + schema.language['cli'] = { name: 'is_autorest_very_cool' }; + Helper.applyNamingConvention(settings, schema, 'cli'); + assert.equal(schema.language['cli'].name, 'is_AUTOREST_very_cool'); + + let prop: Property = new Property('hello_world', 'desc', null); + prop.language['cli'] = { name: 'hello_world' }; + Helper.applyNamingConvention(settings, prop, 'cli'); + assert.equal(prop.language['cli'].name, 'hello-world'); + + let choice: ChoiceSchema = new ChoiceSchema('name', 'desc'); + choice.language['cli'] = { name: 'all_dogs_are_animal' } + Helper.applyNamingConvention(settings, choice, 'cli'); + assert.equal(choice.language['cli'].name, 'all dogs are animal'); + + let value: ChoiceValue = new ChoiceValue('name', 'desc', null); + choice.language['cli'] = { name: 'all_dogs_are_animal' } + Helper.applyNamingConvention(settings, choice, 'cli'); + assert.equal(choice.language['cli'].name, 'all dogs are animal'); + + }); +}); \ No newline at end of file