From 67674b25809b83fce7a4a0f77c22a03908148a36 Mon Sep 17 00:00:00 2001 From: xichen Date: Wed, 29 Jul 2020 10:10:51 +0800 Subject: [PATCH] Support enum (#643) * support enum * fix diff from m3 * llcsharp-v2 should rely on modifiers-v2 Co-authored-by: xichen Co-authored-by: Xiaogang --- powershell/autorest-configuration.md | 7 +- powershell/cmdlets/class.ts | 2 +- powershell/enums/namespace.ts | 202 ++--- powershell/llcsharp/enums/enum.ts | 171 ++++- powershell/llcsharp/model/namespace.ts | 12 +- powershell/llcsharp/operation/method.ts | 12 +- powershell/llcsharp/schema/enum.ts | 4 +- powershell/llcsharp/schema/schema-resolver.ts | 4 +- powershell/llcsharp/schema/string.ts | 33 +- powershell/main.ts | 2 + powershell/plugins/create-commands-v2.ts | 1 + powershell/plugins/cs-namer-v2.ts | 65 +- powershell/plugins/modifiers-v2.ts | 716 ++++++++++++++++++ powershell/plugins/plugin-tweak-model.ts | 4 +- powershell/utils/schema.ts | 12 + tests-upgrade/Configuration.json | 6 +- 16 files changed, 1082 insertions(+), 171 deletions(-) create mode 100644 powershell/plugins/modifiers-v2.ts diff --git a/powershell/autorest-configuration.md b/powershell/autorest-configuration.md index 076d7cb7f65..e146910a3c7 100644 --- a/powershell/autorest-configuration.md +++ b/powershell/autorest-configuration.md @@ -137,11 +137,14 @@ pipeline: psnamer-v2: input: csnamer-v2 - add-azure-completers-v2: + modifiers-v2: input: psnamer-v2 + add-azure-completers-v2: + input: modifiers-v2 + llcsharp-v2: - input: add-azure-completers-v2 + input: modifiers-v2 powershell-v2: input: add-azure-completers-v2 diff --git a/powershell/cmdlets/class.ts b/powershell/cmdlets/class.ts index 819ecbe673d..41c630537af 100644 --- a/powershell/cmdlets/class.ts +++ b/powershell/cmdlets/class.ts @@ -2580,7 +2580,7 @@ export class NewCmdletClass extends Class { regularCmdletParameter.add(new Attribute(AllowEmptyCollectionAttribute)); } - NewAddInfoAttribute(regularCmdletParameter, propertyType, vParam.required ?? false, false, vParam.description, origin.details.default.serializedName); + NewAddInfoAttribute(regularCmdletParameter, propertyType, vParam.required ?? false, false, vParam.description, origin.name); NewAddCompleterInfo(regularCmdletParameter, vParam); addDefaultInfo(regularCmdletParameter, vParam); diff --git a/powershell/enums/namespace.ts b/powershell/enums/namespace.ts index a3b07985233..8350aad788c 100644 --- a/powershell/enums/namespace.ts +++ b/powershell/enums/namespace.ts @@ -10,6 +10,8 @@ import { IArgumentCompleter, CompletionResult, CommandAst, CompletionResultType, import { join } from 'path'; import { DeepPartial } from '@azure-tools/codegen'; +import { EnumDetails as NewEnumDetails } from '../utils/schema'; + export class EnumNamespace extends Namespace { public get outputFolder(): string { return join(this.state.project.apiFolder, 'Support'); @@ -127,104 +129,106 @@ export class NewEnumNamespace extends Namespace { constructor(parent: Namespace, public state: NewState, objectInitializer?: DeepPartial) { super('Support', parent); this.apply(objectInitializer); - // skip-for-time-being - // const enumInfos = values(state.model.schemas) - // .where(each => each.details.csharp.enum !== undefined && !each.details.csharp.skip) - // .select(each => ({ details: each.details.csharp.enum, description: each.details.csharp.description })) - // .toArray(); - - // const done = new Set(); - - - // for (const enumInfo of enumInfos) { - // if (done.has(enumInfo.details.name)) { - // continue; - // } - - // done.add(enumInfo.details.name); - - // if (state.project.azure && /^api-?version$/i.exec(enumInfo.details.name)) { - // continue; - // } - - // // generate a typeconverter for the enum class too. - - // const enumValues = values(enumInfo.details.values).select(v => v.value).toArray(); - // const enumClass = new Struct(this, enumInfo.details.name, undefined, { - // interfaces: [IArgumentCompleter], - // partial: true, - // description: enumInfo.description || `Argument completer implementation for ${enumInfo.details.name}.`, - // fileName: `${enumInfo.details.name}.Completer` - // }); - // const commandName = new Parameter('commandName', System.String, { description: 'The name of the command that needs argument completion.' }); - // const parameterName = new Parameter('parameterName', System.String, { description: 'The name of the parameter that needs argument completion.' }); - // const wordToComplete = new Parameter('wordToComplete', System.String, { description: 'The (possibly empty) word being completed.' }); - // const commandAst = new Parameter('commandAst', CommandAst, { description: 'The command ast in case it is needed for completion.' }); - // const fakeBoundParameters = new Parameter('fakeBoundParameters', System.Collections.IDictionary, { description: 'This parameter is similar to $PSBoundParameters, except that sometimes PowerShell cannot or will not attempt to evaluate an argument, in which case you may need to use commandAst.' }); - // const completeArgumentParams = [commandName, parameterName, wordToComplete, commandAst, fakeBoundParameters]; - - // enumClass.add(new Method('CompleteArgument', System.Collections.Generic.IEnumerable(CompletionResult), { parameters: completeArgumentParams, description: 'Implementations of this function are called by PowerShell to complete arguments.', returnsDescription: 'A collection of completion results, most like with ResultType set to ParameterValue.' })).add(function* () { - // for (const enumValue of enumValues) { - // yield If(`${System.String.declaration}.IsNullOrEmpty(${wordToComplete.name}) || "${enumValue}".StartsWith(${wordToComplete.name}, ${System.StringComparison.declaration}.InvariantCultureIgnoreCase)`, - // `yield return new ${CompletionResult.declaration}("${enumValue}", "${enumValue}", ${CompletionResultType.declaration}.ParameterValue, "${enumValue}");`); - // } - // }); - - - // // generate a typeconverter for the enum class too. - - // const converterClass = new Class(this, `${enumInfo.details.name}TypeConverter`, undefined, { - // interfaces: [PSTypeConverter], - // partial: true, - // description: enumInfo.description || `TypeConverter implementation for ${enumInfo.details.name}.`, - // fileName: `${enumInfo.details.name}.TypeConverter` - // }); - - // converterClass.add(new LambdaMethod('CanConvertFrom', dotnet.Bool, dotnet.True, { - // override: Modifier.Override, - // parameters: [ - // new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), - // new Parameter('destinationType', System.Type, { description: 'the to convert to' }) - // ], - // description: 'Determines if the converter can convert the parameter to the parameter.', - // returnsDescription: 'true if the converter can convert the parameter to the parameter, otherwise false.', - // })); - - // converterClass.add(new LambdaMethod('CanConvertTo', dotnet.Bool, dotnet.False, { - // override: Modifier.Override, - // parameters: [ - // new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), - // new Parameter('destinationType', System.Type, { description: 'the to convert to' }) - // ], - // description: 'Determines if the converter can convert the parameter to the parameter.', - // returnsDescription: 'true if the converter can convert the parameter to the parameter, otherwise false.', - // })); - - // converterClass.add(new LambdaMethod('ConvertFrom', dotnet.Object, new LiteralExpression(`${enumInfo.details.name}.CreateFrom(sourceValue)`), { - // override: Modifier.Override, - // parameters: [ - // new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), - // new Parameter('destinationType', System.Type, { description: 'the to convert to' }), - // new Parameter('formatProvider', System.IFormatProvider, { description: 'not used by this TypeConverter.' }), - // new Parameter('ignoreCase', dotnet.Bool, { description: 'when set to true, will ignore the case when converting.' }), - // ], - // description: 'Converts the parameter to the parameter using and ', - // returnsDescription: `an instance of , or null if there is no suitable conversion.` - // })); - - // converterClass.add(new LambdaMethod('ConvertTo', dotnet.Object, dotnet.Null, { - // override: Modifier.Override, - // parameters: [ - // new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), - // new Parameter('destinationType', System.Type, { description: 'the to convert to' }), - // new Parameter('formatProvider', System.IFormatProvider, { description: 'not used by this TypeConverter.' }), - // new Parameter('ignoreCase', dotnet.Bool, { description: 'when set to true, will ignore the case when converting.' }), - // ], description: 'NotImplemented -- this will return null', - // returnsDescription: 'will always return null.' - // })); - - // enumClass.add(new Attribute(TypeConverterAttribute, { parameters: [new LiteralExpression(`typeof(${converterClass})`)] })); - - // } + const enumInfos = [...state.model.schemas.sealedChoices ?? [], ...state.model.schemas.choices ?? []] + .filter((choice) => !choice.language.csharp?.skip) + .map((choice) => { + return { + details: choice.language.csharp?.enum, + description: choice.language.csharp?.description + } + }); + + const done = new Set(); + + for (const enumInfo of enumInfos) { + if (done.has(enumInfo.details.name)) { + continue; + } + + done.add(enumInfo.details.name); + + if (state.project.azure && /^api-?version$/i.exec(enumInfo.details.name)) { + continue; + } + + // generate a typeconverter for the enum class too. + + const enumValues = values(enumInfo.details.values).select(v => v.value).toArray(); + const enumClass = new Struct(this, enumInfo.details.name, undefined, { + interfaces: [IArgumentCompleter], + partial: true, + description: enumInfo.description || `Argument completer implementation for ${enumInfo.details.name}.`, + fileName: `${enumInfo.details.name}.Completer` + }); + const commandName = new Parameter('commandName', System.String, { description: 'The name of the command that needs argument completion.' }); + const parameterName = new Parameter('parameterName', System.String, { description: 'The name of the parameter that needs argument completion.' }); + const wordToComplete = new Parameter('wordToComplete', System.String, { description: 'The (possibly empty) word being completed.' }); + const commandAst = new Parameter('commandAst', CommandAst, { description: 'The command ast in case it is needed for completion.' }); + const fakeBoundParameters = new Parameter('fakeBoundParameters', System.Collections.IDictionary, { description: 'This parameter is similar to $PSBoundParameters, except that sometimes PowerShell cannot or will not attempt to evaluate an argument, in which case you may need to use commandAst.' }); + const completeArgumentParams = [commandName, parameterName, wordToComplete, commandAst, fakeBoundParameters]; + + enumClass.add(new Method('CompleteArgument', System.Collections.Generic.IEnumerable(CompletionResult), { parameters: completeArgumentParams, description: 'Implementations of this function are called by PowerShell to complete arguments.', returnsDescription: 'A collection of completion results, most like with ResultType set to ParameterValue.' })).add(function* () { + for (const enumValue of enumValues) { + yield If(`${System.String.declaration}.IsNullOrEmpty(${wordToComplete.name}) || "${enumValue}".StartsWith(${wordToComplete.name}, ${System.StringComparison.declaration}.InvariantCultureIgnoreCase)`, + `yield return new ${CompletionResult.declaration}("${enumValue}", "${enumValue}", ${CompletionResultType.declaration}.ParameterValue, "${enumValue}");`); + } + }); + + + // generate a typeconverter for the enum class too. + + const converterClass = new Class(this, `${enumInfo.details.name}TypeConverter`, undefined, { + interfaces: [PSTypeConverter], + partial: true, + description: enumInfo.description || `TypeConverter implementation for ${enumInfo.details.name}.`, + fileName: `${enumInfo.details.name}.TypeConverter` + }); + + converterClass.add(new LambdaMethod('CanConvertFrom', dotnet.Bool, dotnet.True, { + override: Modifier.Override, + parameters: [ + new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), + new Parameter('destinationType', System.Type, { description: 'the to convert to' }) + ], + description: 'Determines if the converter can convert the parameter to the parameter.', + returnsDescription: 'true if the converter can convert the parameter to the parameter, otherwise false.', + })); + + converterClass.add(new LambdaMethod('CanConvertTo', dotnet.Bool, dotnet.False, { + override: Modifier.Override, + parameters: [ + new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), + new Parameter('destinationType', System.Type, { description: 'the to convert to' }) + ], + description: 'Determines if the converter can convert the parameter to the parameter.', + returnsDescription: 'true if the converter can convert the parameter to the parameter, otherwise false.', + })); + + converterClass.add(new LambdaMethod('ConvertFrom', dotnet.Object, new LiteralExpression(`${enumInfo.details.name}.CreateFrom(sourceValue)`), { + override: Modifier.Override, + parameters: [ + new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), + new Parameter('destinationType', System.Type, { description: 'the to convert to' }), + new Parameter('formatProvider', System.IFormatProvider, { description: 'not used by this TypeConverter.' }), + new Parameter('ignoreCase', dotnet.Bool, { description: 'when set to true, will ignore the case when converting.' }), + ], + description: 'Converts the parameter to the parameter using and ', + returnsDescription: `an instance of , or null if there is no suitable conversion.` + })); + + converterClass.add(new LambdaMethod('ConvertTo', dotnet.Object, dotnet.Null, { + override: Modifier.Override, + parameters: [ + new Parameter('sourceValue', dotnet.Object, { description: 'the to convert from' }), + new Parameter('destinationType', System.Type, { description: 'the to convert to' }), + new Parameter('formatProvider', System.IFormatProvider, { description: 'not used by this TypeConverter.' }), + new Parameter('ignoreCase', dotnet.Bool, { description: 'when set to true, will ignore the case when converting.' }), + ], description: 'NotImplemented -- this will return null', + returnsDescription: 'will always return null.' + })); + + enumClass.add(new Attribute(TypeConverterAttribute, { parameters: [new LiteralExpression(`typeof(${converterClass})`)] })); + + } } } \ No newline at end of file diff --git a/powershell/llcsharp/enums/enum.ts b/powershell/llcsharp/enums/enum.ts index e44e8783673..8f4303e708c 100644 --- a/powershell/llcsharp/enums/enum.ts +++ b/powershell/llcsharp/enums/enum.ts @@ -20,11 +20,13 @@ import { Property } from '@azure-tools/codegen-csharp'; import { OneOrMoreStatements } from '@azure-tools/codegen-csharp'; import { Struct } from '@azure-tools/codegen-csharp'; import { Variable } from '@azure-tools/codegen-csharp'; -import { EnumImplementation } from '../schema/enum'; -import { EnhancedTypeDeclaration } from '../schema/extended-type-declaration'; -import { State } from '../generator'; +import { EnumImplementation, NewEnumImplementation } from '../schema/enum'; +import { EnhancedTypeDeclaration, NewEnhancedTypeDeclaration } from '../schema/extended-type-declaration'; +import { State, NewState } from '../generator'; import { DeepPartial } from '@azure-tools/codegen'; +import { Schema as NewSchema } from '@azure-tools/codemodel'; + export class EnumClass extends Struct implements EnhancedTypeDeclaration { implementation: EnumImplementation; get schema(): Schema { @@ -187,3 +189,166 @@ export class EnumClass extends Struct implements EnhancedTypeDeclaration { return this.implementation.validatePresence(eventListener, property); } } + +export class NewEnumClass extends Struct implements NewEnhancedTypeDeclaration { + implementation: NewEnumImplementation; + get schema(): NewSchema { + return this.implementation.schema; + } + get convertObjectMethod() { + return this.implementation.convertObjectMethod; + } + + get defaultOfType() { + return toExpression('null /* enum value */'); + } + + deserializeFromContainerMember(mediaType: KnownMediaType, container: ExpressionOrLiteral, serializedName: string, defaultValue: Expression): Expression { + return this.implementation.deserializeFromContainerMember(mediaType, container, serializedName, defaultValue); + } + deserializeFromNode(mediaType: KnownMediaType, node: ExpressionOrLiteral, defaultValue: Expression): Expression { + return this.implementation.deserializeFromNode(mediaType, node, defaultValue); + } + /** emits an expression to deserialize content from a string */ + deserializeFromString(mediaType: KnownMediaType, content: ExpressionOrLiteral, defaultValue: Expression): Expression | undefined { + return this.implementation.deserializeFromString(mediaType, content, defaultValue); + } + /** emits an expression to deserialize content from a content/response */ + deserializeFromResponse(mediaType: KnownMediaType, content: ExpressionOrLiteral, defaultValue: Expression): Expression | undefined { + return this.implementation.deserializeFromResponse(mediaType, content, defaultValue); + } + serializeToNode(mediaType: KnownMediaType, value: ExpressionOrLiteral, serializedName: string, mode: Expression): Expression { + return this.implementation.serializeToNode(mediaType, value, serializedName, mode); + } + /** emits an expression serialize this to a HttpContent */ + serializeToContent(mediaType: KnownMediaType, value: ExpressionOrLiteral, mode: Expression): Expression { + return this.implementation.serializeToContent(mediaType, value, mode); + } + + serializeToContainerMember(mediaType: KnownMediaType, value: ExpressionOrLiteral, container: Variable, serializedName: string, mode: Expression): OneOrMoreStatements { + return this.implementation.serializeToContainerMember(mediaType, value, container, serializedName, mode); + } + + get isXmlAttribute(): boolean { + return this.implementation.isXmlAttribute; + } + + get isNullable(): boolean { + return this.implementation.isNullable; + } + + get isRequired(): boolean { + return this.implementation.isRequired; + } + + constructor(schemaWithFeatures: NewEnumImplementation, state: NewState, objectInitializer?: DeepPartial) { + if (!schemaWithFeatures.schema.language.csharp?.enum) { + throw new Error(`ENUM AINT XMSENUM: ${schemaWithFeatures.schema.language.csharp?.name}`); + } + + super(state.project.supportNamespace, schemaWithFeatures.schema.language.csharp?.enum.name, undefined, { + interfaces: [new Interface(new Namespace('System'), 'IEquatable', { + genericParameters: [`${schemaWithFeatures.schema.language.csharp?.enum.name}`] + })], + }); + this.description = schemaWithFeatures.schema.language.csharp?.description; + this.implementation = schemaWithFeatures; + this.partial = true; + + this.apply(objectInitializer); + + // add known enum values + for (const evalue of schemaWithFeatures.schema.language.csharp?.enum.values) { + this.addField(new Field(evalue.name, this, { initialValue: new StringExpression(evalue.value), static: Modifier.Static, description: evalue.description })); + } + + // add backingField + const backingField = this.add(new Property('_value', dotnet.String, { + getAccess: Access.Private, + setAccess: Access.Private, + description: `the value for an instance of the Enum.` + })); + + // add private constructor + const p = new Parameter('underlyingValue', dotnet.String, { description: 'the value to create an instance for.' }); + const ctor = this.addMethod(new Constructor(this, { + access: Access.Private, + parameters: [p], + description: `Creates an instance of the ` + })).add(`this.${backingField.value} = ${p.value};`); + + // add toString Method + this.addMethod(new Method('ToString', dotnet.String, { + override: Modifier.Override, + description: `Returns string representation for ${this.name}`, + returnsDescription: 'A string for this value.' + })).add(`return this.${backingField.value};`); + + // add Equals Method(thistype) + this.addMethod(new Method('Equals', dotnet.Bool, { + description: `Compares values of enum type ${this.name}`, + parameters: [new Parameter('e', this, { description: 'the value to compare against this instance.' })], + returnsDescription: 'true if the two instances are equal to the same value' + })).add(`return ${backingField.value}.Equals(e.${backingField.value});`); + + // add Equals Method(object) + this.addMethod(new Method('Equals', dotnet.Bool, { + override: Modifier.Override, + description: `Compares values of enum type ${this.name} (override for Object)`, + parameters: [new Parameter('obj', dotnet.Object, { description: 'the value to compare against this instance.' })], + returnsDescription: 'true if the two instances are equal to the same value' + })).add(`return obj is ${this.name} && Equals((${this.name})obj);`); + + // add implicit operator(string) + this.addMethod(new Operator(`implicit operator ${this.name}`, { + static: Modifier.Static, + description: `Implicit operator to convert string to ${this.name}`, + parameters: [new Parameter('value', dotnet.String, { description: `the value to convert to an instance of .` })] + })).add(`return new ${this.name}(value);`); + + // add static creation + this.addMethod(new Method('CreateFrom', dotnet.Object, { + static: Modifier.Static, + access: Access.Internal, + description: `Conversion from arbitrary object to ${this.name}`, + parameters: [new Parameter('value', dotnet.Object, { description: `the value to convert to an instance of .` })] + })).add(`return new ${this.name}(System.Convert.ToString(value));`); + + // add implicit operator(thistype) + this.addMethod(new Operator('implicit operator string', { + static: Modifier.Static, + description: `Implicit operator to convert ${this.name} to string`, + parameters: [new Parameter('e', this, { description: `the value to convert to an instance of .` })] + })).add(`return e.${backingField.value};`); + + // add operator == + this.addMethod(new Method('operator ==', dotnet.Bool, { + static: Modifier.Static, + description: `Overriding == operator for enum ${this.name}`, + parameters: [new Parameter('e1', this, { description: 'the value to compare against ' }), new Parameter('e2', this, { description: 'the value to compare against ' })], + returnsDescription: 'true if the two instances are equal to the same value' + })).add('return e2.Equals(e1);'); + + // add opeator != + this.addMethod(new Method('operator !=', dotnet.Bool, { + static: Modifier.Static, + description: `Overriding != operator for enum ${this.name}`, + parameters: [new Parameter('e1', this, { description: 'the value to compare against ' }), new Parameter('e2', this, { description: 'the value to compare against ' })], + returnsDescription: 'true if the two instances are not equal to the same value' + })).add('return !e2.Equals(e1);'); + + // add getHashCode + this.addMethod(new Method('GetHashCode', dotnet.Int, { + override: Modifier.Override, + description: `Returns hashCode for enum ${this.name}`, + returnsDescription: 'The hashCode of the value' + })).add(`return this.${backingField.value}.GetHashCode();`); + } + + public validateValue(eventListener: Variable, property: Variable): OneOrMoreStatements { + return this.implementation.validateValue(eventListener, property); + } + public validatePresence(eventListener: Variable, property: Variable): OneOrMoreStatements { + return this.implementation.validatePresence(eventListener, property); + } +} \ No newline at end of file diff --git a/powershell/llcsharp/model/namespace.ts b/powershell/llcsharp/model/namespace.ts index c651d1befb2..a437aa8ce57 100644 --- a/powershell/llcsharp/model/namespace.ts +++ b/powershell/llcsharp/model/namespace.ts @@ -13,7 +13,7 @@ import { EnumImplementation, NewEnumImplementation } from '../schema/enum'; import { EnhancedTypeDeclaration, NewEnhancedTypeDeclaration } from '../schema/extended-type-declaration'; import { ObjectImplementation, NewObjectImplementation } from '../schema/object'; import { SchemaDefinitionResolver, NewSchemaDefinitionResolver } from '../schema/schema-resolver'; -import { EnumClass } from '../enums/enum'; +import { EnumClass, NewEnumClass } from '../enums/enum'; import * as validation from '../validations'; import { ModelInterface, NewModelInterface } from './interface'; import { ModelClass, NewModelClass } from './model-class'; @@ -146,9 +146,8 @@ export class ModelsNamespace extends Namespace { if (schema.language.csharp?.enum) { const ec = state.project.supportNamespace.findClassByName(schema.language.csharp.enum.name); if (length(ec) === 0) { - // skip-for-time-being - //new EnumClass(td, state); - // return schema.details.csharp.typeDeclaration = ec[0]; + new NewEnumClass(td, state); + // return schema.language.csharp.typeDeclaration = ec[0]; } } schema.language.csharp = schema.language.csharp || new Language(); @@ -223,9 +222,8 @@ export class NewModelsNamespace extends Namespace { if (schema.language.csharp?.enum) { const ec = state.project.supportNamespace.findClassByName(schema.language.csharp.enum.name); if (length(ec) === 0) { - // skip-for-time-being - //new EnumClass(td, state); - // return schema.details.csharp.typeDeclaration = ec[0]; + new NewEnumClass(td, state); + // return schema.language.csharp.typeDeclaration = ec[0]; } } schema.language.csharp = schema.language.csharp || new Language(); diff --git a/powershell/llcsharp/operation/method.ts b/powershell/llcsharp/operation/method.ts index a1db05fac8e..bf7a4d05bfa 100644 --- a/powershell/llcsharp/operation/method.ts +++ b/powershell/llcsharp/operation/method.ts @@ -400,20 +400,20 @@ export class NewOperationMethod extends Method { // replace any server params in the uri for (const pp of serverParams) { - url = url.replace(`{${pp.param.language.default.name}}`, `" + url = url.replace(`{${pp.param.language.csharp?.name}}`, `" + ${pp.name} + "`); } for (const pp of pathParams) { - rx = rx.replace(`{${pp.param.language.default.name}}`, `(?<${pp.param.language.default.name}>[^/]+)`); + rx = rx.replace(`{${pp.param.language.csharp?.name}}`, `(?<${pp.param.language.csharp?.name}>[^/]+)`); if (this.viaIdentity) { - url = url.replace(`{${pp.param.language.default.name}}`, `" + url = url.replace(`{${pp.param.language.csharp?.name}}`, `" + ${pp.name} + "`); } else { - url = url.replace(`{${pp.param.language.default.name}}`, `" + url = url.replace(`{${pp.param.language.csharp?.name}}`, `" + ${newRemoveEncoding(pp, '', KnownMediaType.UriParameter)} + "`); } @@ -439,7 +439,7 @@ export class NewOperationMethod extends Method { 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.name}"].Value;`; + yield `var ${pp.name} = ${match.value}.Groups["${pp.param.language.csharp?.name}"].Value;`; } } @@ -473,7 +473,7 @@ export class NewOperationMethod extends Method { // 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.name, ClientRuntime.SerializationMode.None); + yield hp.serializeToContainerMember(KnownMediaType.Header, new LocalVariable('request.Headers', dotnet.Var), hp.param.language.csharp?.name || hp.param.language.default.name, ClientRuntime.SerializationMode.None); } yield EOL; } diff --git a/powershell/llcsharp/schema/enum.ts b/powershell/llcsharp/schema/enum.ts index fdd7f4d1088..bf80b84f77e 100644 --- a/powershell/llcsharp/schema/enum.ts +++ b/powershell/llcsharp/schema/enum.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Schema } from '../code-model'; -import { StringSchema } from '@azure-tools/codemodel'; +import { Schema as NewSchema } from '@azure-tools/codemodel'; import { String, NewString } from './string'; import { dotnet, toExpression } from '@azure-tools/codegen-csharp'; @@ -36,7 +36,7 @@ export class NewEnumImplementation extends NewString { return !this.isRequired; } - constructor(schema: StringSchema, isRequired: boolean) { + constructor(schema: NewSchema, isRequired: boolean) { super(schema, isRequired); } diff --git a/powershell/llcsharp/schema/schema-resolver.ts b/powershell/llcsharp/schema/schema-resolver.ts index e0094acf4ca..e175248d98e 100644 --- a/powershell/llcsharp/schema/schema-resolver.ts +++ b/powershell/llcsharp/schema/schema-resolver.ts @@ -222,7 +222,9 @@ export class NewSchemaDefinitionResolver { state.error(`Unsupported constant type. Schema '${schema.language.csharp?.name}' is declared with invalid type '${schema.type}'`, message.UnknownJsonType); throw new Error('Unknown Model. Fatal.'); } - + case SchemaType.Choice: + case SchemaType.SealedChoice: + return new NewEnumImplementation(schema, required); case undefined: if (schema.extensions && schema.extensions['x-ms-enum']) { return new NewEnumImplementation(schema, required); diff --git a/powershell/llcsharp/schema/string.ts b/powershell/llcsharp/schema/string.ts index 110a9d4ee46..8d8316e78fe 100644 --- a/powershell/llcsharp/schema/string.ts +++ b/powershell/llcsharp/schema/string.ts @@ -12,7 +12,7 @@ import { OneOrMoreStatements } from '@azure-tools/codegen-csharp'; import { Variable } from '@azure-tools/codegen-csharp'; import { ClientRuntime } from '../clientruntime'; import { Schema } from '../code-model'; -import { Schema as NewSchema, StringSchema } from '@azure-tools/codemodel'; +import { ChoiceSchema, Schema as NewSchema, StringSchema } from '@azure-tools/codemodel'; import { popTempVar, pushTempVar } from './primitive'; import { EnhancedTypeDeclaration, NewEnhancedTypeDeclaration } from './extended-type-declaration'; import { length } from '@azure-tools/linq'; @@ -354,7 +354,7 @@ export class NewString implements NewEnhancedTypeDeclaration { return (`/* serializeToContainerMember doesn't support '${mediaType}' ${__filename}*/`); } - constructor(public schema: StringSchema, public isRequired: boolean) { + constructor(public schema: NewSchema, public isRequired: boolean) { } @@ -367,6 +367,7 @@ export class NewString implements NewEnhancedTypeDeclaration { ${this.validateMinLength(eventListener, property)} ${this.validateMaxLength(eventListener, property)} ${this.validateRegex(eventListener, property)} +${this.validateEnum(eventListener, property)} `.trim(); } @@ -376,27 +377,31 @@ ${this.validateRegex(eventListener, property)} } private validateMinLength(eventListener: Variable, property: Variable): string { - if (!this.schema.minLength) { + const len = (this.schema).minLength; + if (!len) { return ''; } - return `await ${eventListener}.AssertMinimumLength(${nameof(property.value)},${property},${this.schema.minLength});`; + return `await ${eventListener}.AssertMinimumLength(${nameof(property.value)},${property},${len});`; } private validateMaxLength(eventListener: Variable, property: Variable): string { - if (!this.schema.maxLength) { + const len = (this.schema).maxLength; + if (!len) { return ''; } - return `await ${eventListener}.AssertMaximumLength(${nameof(property.value)},${property},${this.schema.maxLength});`; + return `await ${eventListener}.AssertMaximumLength(${nameof(property.value)},${property},${len});`; } private validateRegex(eventListener: Variable, property: Variable): string { - if (!this.schema.pattern) { + const pattern = (this.schema).pattern; + if (!pattern) { return ''; } - return `await ${eventListener}.AssertRegEx(${nameof(property.value)},${property},@"${this.schema.pattern}");`; + return `await ${eventListener}.AssertRegEx(${nameof(property.value)},${property},@"${pattern}");`; + } + private validateEnum(eventListener: Variable, property: Variable): string { + if (!(this.schema instanceof ChoiceSchema)) { + return ''; + } + const choiceValues = this.schema.choices.map((c) => c.value); + return `await ${eventListener}.AssertEnum(${nameof(property.value)},${property},${choiceValues.joinWith((v) => `@"${v}"`)});`; } - // private validateEnum(eventListener: Variable, property: Variable): string { - // if (!this.schema.enum || length(this.schema.enum) === 0) { - // return ''; - // } - // return `await ${eventListener}.AssertEnum(${nameof(property.value)},${property},${this.schema.enum.joinWith((v) => `@"${v}"`)});`; - // } } diff --git a/powershell/main.ts b/powershell/main.ts index c2764a4af99..5aa78458fc3 100644 --- a/powershell/main.ts +++ b/powershell/main.ts @@ -20,6 +20,7 @@ import { namerV2 } from './plugins/ps-namer-v2'; import { llcsharpV2 } from './plugins/llcsharp-v2'; import { powershellV2 } from './plugins/powershell-v2'; import { addCompleterV2 } from './plugins/add-azure-completers-v2'; +import { applyModifiersV2 } from './plugins/modifiers-v2'; require('source-map-support').install(); @@ -39,6 +40,7 @@ export async function main() { pluginHost.Add('create-commands-v2', createCommandsV2); pluginHost.Add('csnamer-v2', csnamerV2); pluginHost.Add('psnamer-v2', namerV2); + pluginHost.Add('modifiers-v2', applyModifiersV2); pluginHost.Add('add-azure-completers-v2', addCompleterV2); pluginHost.Add('llcsharp-v2', llcsharpV2); pluginHost.Add('powershell-v2', powershellV2); diff --git a/powershell/plugins/create-commands-v2.ts b/powershell/plugins/create-commands-v2.ts index aab456f6873..ca67a3e2199 100644 --- a/powershell/plugins/create-commands-v2.ts +++ b/powershell/plugins/create-commands-v2.ts @@ -393,6 +393,7 @@ export /* @internal */ class Inferrer { 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 diff --git a/powershell/plugins/cs-namer-v2.ts b/powershell/plugins/cs-namer-v2.ts index c3929d5df70..3fd5e67f59a 100644 --- a/powershell/plugins/cs-namer-v2.ts +++ b/powershell/plugins/cs-namer-v2.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { codeModelSchema, SchemaResponse, CodeModel, Schema, ObjectSchema, GroupSchema, isObjectSchema, SchemaType, GroupProperty, ParameterLocation, Operation, Parameter, VirtualParameter, getAllProperties, ImplementationLocation, OperationGroup, Request, SchemaContext } from '@azure-tools/codemodel'; +import { codeModelSchema, SchemaResponse, CodeModel, Schema, ObjectSchema, GroupSchema, isObjectSchema, SchemaType, GroupProperty, ParameterLocation, Operation, Parameter, VirtualParameter, getAllProperties, ImplementationLocation, OperationGroup, Request, SchemaContext, StringSchema, ChoiceSchema, SealedChoiceSchema } from '@azure-tools/codemodel'; //import { codemodel, JsonType, ModelState, processCodeModel, VirtualProperty } from '@azure-tools/codemodel-v3'; import { camelCase, deconstruct, excludeXDash, fixLeadingNumber, pascalCase, lowest, maximum, minimum, getPascalIdentifier, serialize } from '@azure-tools/codegen'; import { items, values, keys, Dictionary, length } from '@azure-tools/linq'; @@ -14,6 +14,7 @@ import { SchemaDetails } from '../llcsharp/code-model'; import { SchemaDefinitionResolver, NewSchemaDefinitionResolver } from '../llcsharp/schema/schema-resolver'; import { PwshModel } from '../utils/PwshModel'; import { NewModelState } from '../utils/model-state'; +import { SchemaDetails as NewSchemaDetails } from '../utils/schema'; type State = NewModelState; @@ -106,27 +107,27 @@ function setSchemaNames(schemaGroups: Dictionary>, azure: boolean, namespace: pascalCase([serviceNamespace, '.', 'Models', ...ns]), // objects have a namespace fullname: `${pascalCase([serviceNamespace, '.', 'Models', ...ns])}.${getPascalIdentifier(schemaName)}`, }; - } else if (schema.type === SchemaType.String && schema.language.default.enum) { + } else if (schema.type === SchemaType.Choice || schema.type === SchemaType.SealedChoice) { // oh, it's an enum type - // Skip for time-being - // schema.language.csharp = { - // ...details, - // interfaceName: pascalCase(fixLeadingNumber(['I', ...deconstruct(schemaName)])), - // name: getPascalIdentifier(schema.language.default.enum.name), - // namespace: pascalCase([serviceNamespace, '.', 'Support']), - // fullname: `${pascalCase([serviceNamespace, '.', 'Support'])}.${getPascalIdentifier(schema.language.default.enum.name)}`, - // enum: { - // ...schema.language.default.enum, - // name: getPascalIdentifier(schema.language.default.enum.name), - // values: schema.language.default.enum.values.map(each => { - // return { - // ...each, - // name: getPascalIdentifier(each.name), - // description: each.description - // }; - // }) - // } - // }; + const choiceSchema = | SealedChoiceSchema>schema; + schema.language.csharp = { + ...details, + interfaceName: pascalCase(fixLeadingNumber(['I', ...deconstruct(schemaName)])), + name: getPascalIdentifier(schemaName), + namespace: pascalCase([serviceNamespace, '.', 'Support']), + fullname: `${pascalCase([serviceNamespace, '.', 'Support'])}.${getPascalIdentifier(schemaName)}`, + enum: { + ...schema.language.default.enum, + name: getPascalIdentifier(schema.language.default.name), + values: choiceSchema.choices.map(each => { + return { + ...each, + name: getPascalIdentifier(each.language.default.name), + description: each.language.default.description + }; + }) + } + }; } else { schema.language.csharp = { ...details, @@ -139,21 +140,17 @@ function setSchemaNames(schemaGroups: Dictionary>, azure: boolean, } // name each property in this schema - setPropertyNames(schema); + setPropertyNames(schema); // fix enum names - // skip-for-time-being - // if (schema.details.default.enum) { - // schema.details.csharp.enum = { - // ...schema.details.default.enum, - // name: getPascalIdentifier(schema.details.default.enum.name) - // }; - - // // and the value names themselves - // for (const value of values(schema.details.csharp.enum.values)) { - // value.name = getPascalIdentifier(value.name); - // } - // } + if (schema.type === SchemaType.Choice || schema.type === SchemaType.SealedChoice) { + schema.language.csharp.enum.name = getPascalIdentifier(schema.language.default.name); + + // and the value names themselves + for (const value of values(schema.language.csharp.enum.values)) { + (value).name = getPascalIdentifier((value).name); + } + } } } diff --git a/powershell/plugins/modifiers-v2.ts b/powershell/plugins/modifiers-v2.ts new file mode 100644 index 00000000000..7ad674ad718 --- /dev/null +++ b/powershell/plugins/modifiers-v2.ts @@ -0,0 +1,716 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// import { codemodel, processCodeModel, allVirtualParameters, allVirtualProperties, ModelState, command } from '@azure-tools/codemodel-v3'; +import { Host, Channel } from '@azure-tools/autorest-extension-base'; +import { pascalCase, serialize } from '@azure-tools/codegen'; +import { items, values, keys, Dictionary, length } from '@azure-tools/linq'; +import { stat } from 'fs'; +import { CommandOperation } from '../utils/command-operation'; +// import { CommandOperation } from '@azure-tools/codemodel-v3/dist/code-model/command-operation'; +import { NewModelState } from '../utils/model-state'; +import { PwshModel } from '../utils/PwshModel'; +import { allVirtualParameters, allVirtualProperties } from '../utils/resolve-conflicts'; +import { EnumValue } from '../utils/schema'; + +type State = NewModelState; + +let directives: Array = []; + +interface WhereCommandDirective { + select?: string; + where: { + 'subject'?: string; + 'subject-prefix'?: string; + 'verb'?: string; + 'variant'?: string; + 'parameter-name'?: string; + }; + set?: { + 'alias': Array | string; + 'subject'?: string; + 'subject-prefix'?: string; + 'verb'?: string; + 'variant'?: string; + 'parameter-name'?: string; + 'parameter-description'?: string; + 'completer'?: { + name: string; + description: string; + script: string; + }; + 'default'?: { + name: string; + description: string; + script: string; + }; + }; + 'clear-alias': boolean; + hide?: boolean; +} +interface RemoveCommandDirective { + select?: string; + where: { + 'subject'?: string; + 'subject-prefix'?: string; + 'verb'?: string; + 'variant'?: string; + 'parameter-name'?: string; + }; + remove: boolean; +} + + +function hasSpecialChars(str: string): boolean { + return !/^[a-zA-Z0-9]+$/.test(str); +} + + +function getFilterError(whereObject: any, prohibitedFilters: Array, selectionType: string): string { + let error = ''; + for (const each of values(prohibitedFilters)) { + if (whereObject[each] !== undefined) { + error += `Can't filter by ${each} when selecting command. `; + } + } + + return error; +} + +function getSetError(setObject: any, prohibitedSetters: Array, selectionType: string): string { + let error = ''; + for (const each of values(prohibitedSetters)) { + if (setObject[each] !== undefined) { + error += `Can't set ${each} when a ${selectionType} is selected. `; + } + } + + return error; +} + + +function isWhereCommandDirective(it: any): it is WhereCommandDirective { + const directive = it; + const select = directive.select; + const where = directive.where; + const set = directive.set; + if (directive.remove === undefined && where && (where.verb || where.variant || where['parameter-name'] || where.subject || where['subject-prefix'] || directive.hide || select === 'command' || select === 'parameter' || directive['clear-alias'])) { + const prohibitedFilters = ['model-name', 'property-name', 'enum-name', 'enum-value-name']; + let error = getFilterError(where, prohibitedFilters, 'command'); + + if (set !== undefined) { + const prohibitedSetters = ['property-name', 'property-description', ' model-name', 'enum-name', 'enum-value-name']; + error += getSetError(set, prohibitedSetters, 'command'); + } + + if (error) { + throw Error(`Incorrect Directive: ${JSON.stringify(it, null, 2)}. Reason: ${error}.`); + } + + return true; + } + + return false; +} + +function isRemoveCommandDirective(it: any): it is RemoveCommandDirective { + const directive = it; + const where = directive.where; + const remove = directive.remove; + if (where && remove && (where.subject || where.verb || where.variant || where['subject-prefix'] || where['parameter-name'] || directive.select === 'command') && directive.select !== 'parameter') { + return true; + } + + return false; +} + + +interface WhereModelDirective { + select?: string; + where: { + 'model-name'?: string; + 'model-fullname'?: string; + 'model-namespace'?: string; + 'property-name'?: string; + }; + set: { + 'suppress-format'?: boolean; + 'format-table'?: { + 'properties'?: Array; + 'exclude-properties'?: Array; + 'labels'?: { [name: string]: string }; + 'width'?: { [name: string]: number }; + }; + 'model-name'?: string; + 'property-name'?: string; + 'property-description'?: string; + }; +} + +function isWhereModelDirective(it: any): it is WhereModelDirective { + const directive = it; + const where = directive.where; + const set = directive.set; + + + if (where && set && (where['model-name'] || where['model-fullname'] || where['model-namespace'] || where['property-name'] || directive.select === 'model')) { + const prohibitedFilters = ['enum-name', 'enum-value-name', 'subject', 'subject-prefix', 'verb', 'variant', 'parameter-name']; + let error = getFilterError(where, prohibitedFilters, 'enum'); + const prohibitedSetters = ['enum-name', 'enum-value-name', 'subject', 'subject-prefix', 'verb', 'variant', 'parameter-name', 'parameter-description', 'completer', 'default']; + error += getSetError(set, prohibitedSetters, 'enum'); + const modelSelectNameConflict = []; + let modelSelectNameType = ''; + if (where['model-name']) { + modelSelectNameType = 'model-name'; + if (where['model-fullname']) { + modelSelectNameConflict.push('model-fullname'); + } + + if (where['model-namespace']) { + modelSelectNameConflict.push('model-namespace'); + } + } else if (where['model-fullname']) { + modelSelectNameType = 'model-fullname'; + if (where['model-name']) { + modelSelectNameConflict.push('model-name'); + } + + if (where['model-namespace']) { + modelSelectNameConflict.push('model-namespace'); + } + } else if (where['model-namespace']) { + modelSelectNameType = 'model-namespace'; + if (where['model-fullname']) { + modelSelectNameConflict.push('model-fullname'); + } + + if (where['model-name']) { + modelSelectNameConflict.push('model-name'); + } + } + + if (length(modelSelectNameConflict) > 0) { + error += `Can't select ${modelSelectNameType} and ${modelSelectNameConflict} at the same time`; + } + + if (error) { + throw Error(`Incorrect Directive: ${JSON.stringify(it, null, 2)}.Reason: ${error}.`); + } + + return true; + + } + return false; +} + +interface WhereEnumDirective { + select?: string; + where: { + 'enum-name'?: string; + 'enum-value-name'?: string; + }; + set: { + 'enum-name'?: string; + 'enum-value-name'?: string; + }; +} + +function isWhereEnumDirective(it: any): it is WhereEnumDirective { + const directive = it; + const where = directive.where; + const set = directive.set; + if (where && set && (where['enum-name'] || where['enum-value-name'] || directive.select === 'enum')) { + const prohibitedFilters = ['model-name', 'property-name', 'subject', 'subject-prefix', 'verb', 'variant', 'parameter-name']; + let error = getFilterError(where, prohibitedFilters, 'enum'); + const prohibitedSetters = ['model-name', 'property-name', 'subject', 'subject-prefix', 'verb', 'variant', 'parameter-name', 'parameter-description', 'completer', 'default']; + error += getSetError(set, prohibitedSetters, 'enum'); + if (error) { + throw Error(`Incorrect Directive: ${JSON.stringify(it, null, 2)}. Reason: ${error}.`); + } + + return true; + } + return false; +} + +async function tweakModel(state: State): Promise { + + // only look at directives without the `transform` node. + // dolauli for directives with transform are implemented in autorest core + for (const directive of directives.filter(each => !each.transform)) { + const getPatternToMatch = (selector: string | undefined): RegExp | undefined => { + return selector ? !hasSpecialChars(selector) ? new RegExp(`^${selector}$`, 'gi') : new RegExp(selector, 'gi') : undefined; + }; + + if (isWhereCommandDirective(directive)) { + const selectType = directive.select; + const clearAlias = directive['clear-alias']; + const subjectRegex = getPatternToMatch(directive.where['subject']); + const subjectPrefixRegex = getPatternToMatch(directive.where['subject-prefix']); + const verbRegex = getPatternToMatch(directive.where.verb); + const variantRegex = getPatternToMatch(directive.where.variant); + const parameterRegex = getPatternToMatch(directive.where['parameter-name']); + + const alias = + (directive.set !== undefined) ? + (directive.set.alias !== undefined) ? + !Array.isArray(directive.set.alias) ? + [directive.set.alias] + : directive.set.alias + : undefined + : undefined; + const subjectReplacer = (directive.set !== undefined) ? directive.set['subject'] : undefined; + const subjectPrefixReplacer = (directive.set !== undefined) ? directive.set['subject-prefix'] : undefined; + const verbReplacer = (directive.set !== undefined) ? directive.set.verb : undefined; + const variantReplacer = (directive.set !== undefined) ? directive.set.variant : undefined; + const parameterReplacer = (directive.set !== undefined) ? directive.set['parameter-name'] : undefined; + const paramDescriptionReplacer = (directive.set !== undefined) ? directive.set['parameter-description'] : undefined; + const paramCompleterReplacer = (directive.set !== undefined) ? directive.set['completer'] : undefined; + const paramDefaultReplacer = (directive.set !== undefined) ? directive.set['default'] : undefined; + + // select all operations + let operations: Array = values(state.model.commands.operations).toArray(); + if (subjectRegex) { + operations = values(operations) + .where(operation => + !!`${operation.details.csharp.subject}`.match(subjectRegex)) + .toArray(); + } + + if (subjectPrefixRegex) { + operations = values(operations) + .where(operation => + !!`${operation.details.csharp.subjectPrefix}`.match(subjectPrefixRegex)) + .toArray(); + } + + if (verbRegex) { + operations = values(operations) + .where(operation => + !!`${operation.details.csharp.verb}`.match(verbRegex)) + .toArray(); + } + + if (variantRegex) { + operations = values(operations) + .where(operation => + !!`${operation.details.csharp.name}`.match(variantRegex)) + .toArray(); + } + + if (parameterRegex && selectType === 'command') { + operations = values(operations) + .where(operation => values(allVirtualParameters(operation.details.csharp.virtualParameters)) + .any(parameter => !!`${parameter.name}`.match(parameterRegex))) + .toArray(); + } + + if (parameterRegex && (selectType === undefined || selectType === 'parameter')) { + const parameters = values(operations) + .selectMany(operation => allVirtualParameters(operation.details.csharp.virtualParameters)) + .where(parameter => !!`${parameter.name}`.match(parameterRegex)) + .toArray(); + for (const p of values(parameters)) { + const parameter = p; + const prevName = parameter.name; + parameter.name = parameterReplacer ? parameterRegex ? parameter.name.replace(parameterRegex, parameterReplacer) : parameterReplacer : parameter.name; + parameter.description = paramDescriptionReplacer ? paramDescriptionReplacer : parameter.description; + parameter.completerInfo = paramCompleterReplacer ? paramCompleterReplacer : parameter.completerInfo; + parameter.defaultInfo = paramDefaultReplacer ? paramDefaultReplacer : parameter.defaultInfo; + + if (clearAlias) { + parameter.alias = []; + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Cleared aliases from parameter ${parameter.name}.` + }); + } + + if (alias) { + const parsedAlias = new Array(); + for (const each of values(alias)) { + parsedAlias.push(hasSpecialChars(each) ? prevName.replace(parameterRegex, each) : each); + } + + parameter.alias = [...new Set(values(parameter.alias, parsedAlias).toArray())]; + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Added alias ${parsedAlias} to parameter ${parameter.name}.` + }); + } + + if (parameterReplacer) { + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Changed parameter-name from ${prevName} to ${parameter.name}.` + }); + } + + if (paramDescriptionReplacer) { + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Set parameter-description from parameter ${parameter.name}.` + }); + } + } + + } else if (operations) { + for (const operation of values(operations)) { + const getCmdletName = (verb: string, subjectPrefix: string, subject: string, variantName: string): string => { + return `${verb}-${subjectPrefix}${subject}${variantName ? `_${variantName}` : ''}`; + }; + + const prevSubject = operation.details.csharp.subject; + const prevSubjectPrefix = operation.details.csharp.subjectPrefix; + const prevVerb = operation.details.csharp.verb; + const prevVariantName = operation.details.csharp.name; + const oldCommandName = getCmdletName(prevVerb, prevSubjectPrefix, prevSubject, prevVariantName); + + // set values + operation.details.csharp.subject = subjectReplacer ? subjectRegex ? prevSubject.replace(subjectRegex, subjectReplacer) : subjectReplacer : prevSubject; + operation.details.csharp.subjectPrefix = subjectPrefixReplacer !== undefined ? subjectPrefixRegex ? prevSubjectPrefix.replace(subjectPrefixRegex, subjectPrefixReplacer) : subjectPrefixReplacer : prevSubjectPrefix; + operation.details.csharp.verb = verbReplacer ? verbRegex ? prevVerb.replace(verbRegex, verbReplacer) : verbReplacer : prevVerb; + operation.details.csharp.name = variantReplacer ? variantRegex ? prevVariantName.replace(variantRegex, variantReplacer) : variantReplacer : prevVariantName; + operation.details.csharp.hidden = (directive.hide !== undefined) ? !!directive.hide : operation.details.csharp.hidden; + + const newSubject = operation.details.csharp.subject; + const newSubjectPrefix = operation.details.csharp.subjectPrefix; + const newVerb = operation.details.csharp.verb; + const newVariantName = operation.details.csharp.name; + const newCommandName = getCmdletName(newVerb, newSubjectPrefix, newSubject, newVariantName); + + // just the subject prefix can be an empty string + if (subjectPrefixReplacer !== undefined || subjectReplacer || verbReplacer || variantReplacer) { + const modificationMessage = `[DIRECTIVE] Changed command from ${oldCommandName} to ${newCommandName}. `; + state.message({ + Channel: Channel.Debug, Text: modificationMessage + }); + } + + if (clearAlias) { + operation.details.csharp.alias = []; + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Cleared aliases from command ${newCommandName}.` + }); + } + + if (alias) { + const getParsedAlias = (rawAlias: string) => { + return rawAlias.replace('${verb}', operation.details.csharp.verb) + .replace('${subject-prefix}', operation.details.csharp.subjectPrefix) + .replace('${subject}', operation.details.csharp.subject) + .replace('${variant}', operation.details.csharp.name); + }; + + const parsedAlias = new Array(); + for (const each of values(alias)) { + parsedAlias.push(getParsedAlias(each)); + } + + operation.details.csharp.alias = [...new Set(values(operation.details.csharp.alias, parsedAlias).toArray())]; + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Added alias ${parsedAlias} to command ${newCommandName}.` + }); + } + } + } + + continue; + } + + if (isWhereModelDirective(directive)) { + const selectType = directive.select; + const modelNameRegex = getPatternToMatch(directive.where['model-name']); + const modelFullNameRegex = getPatternToMatch(directive.where['model-fullname']); + const modelNamespaceRegex = getPatternToMatch(directive.where['model-namespace']); + const propertyNameRegex = getPatternToMatch(directive.where['property-name']); + + const modelNameReplacer = directive.set['model-name']; + const propertyNameReplacer = directive.set['property-name']; + const propertyDescriptionReplacer = directive.set['property-description']; + const formatTable = directive.set['format-table']; + const suppressFormat = directive.set['suppress-format']; + + // select all models + let models = [...state.model.schemas.objects ?? []]; + // let models = values(state.model.schemas).toArray(); + if (modelNameRegex) { + models = values(models) + .where(model => + !!`${model.language.csharp?.name}`.match(modelNameRegex)) + .toArray(); + } + + if (modelFullNameRegex) { + models = values(models) + .where(model => + !!`${model.language.csharp?.fullname}`.match(modelFullNameRegex)) + .toArray(); + } + + if (modelNamespaceRegex) { + models = values(models) + .where(model => + !!`${model.language.csharp?.namespace}`.match(modelNamespaceRegex)) + .toArray(); + } + + if (propertyNameRegex && selectType === 'model') { + models = values(models) + .where(model => values(allVirtualProperties(model.language.csharp?.virtualProperties)) + .any(property => !!`${property.name}`.match(propertyNameRegex))) + .toArray(); + } + + if (propertyNameRegex && (selectType === undefined || selectType === 'property')) { + const properties = values(models) + .selectMany(model => allVirtualProperties(model.language.csharp?.virtualProperties)) + .where(property => !!`${property.name}`.match(propertyNameRegex)) + .toArray(); + for (const property of values(properties)) { + const prevName = property.name; + property.name = propertyNameReplacer ? propertyNameRegex ? property.name.replace(propertyNameRegex, propertyNameReplacer) : propertyNameReplacer : property.name; + property.description = propertyDescriptionReplacer ? propertyDescriptionReplacer : property.description; + + if (!property.name) { + state.message({ Channel: Channel.Error, Text: `Directive '${directive.where['model-name'] || directive.where['model-fullname']}/${directive.where['property-name']}' attempted to change '${prevName}' to '' ` }); + } + if (propertyNameRegex) { + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Changed property-name from ${prevName} to ${property.name}.` + }); + } + } + + } else if (models) { + for (const model of values(models)) { + + if (suppressFormat && model.language.csharp) { + model.language.csharp.suppressFormat = true; + } + + if (formatTable !== undefined && !suppressFormat) { + const properties = allVirtualProperties(model.language.csharp?.virtualProperties); + const propertiesToExclude = formatTable['exclude-properties']; + const propertiesToInclude = formatTable.properties; + const labels = formatTable.labels; + const widths = formatTable.width; + if (labels) { + const parsedLabels = new Dictionary(); + for (const label of items(labels)) { + parsedLabels[label.key.toLowerCase()] = label.value; + } + + for (const property of values(properties)) { + if (Object.keys(parsedLabels).includes(property.name.toLowerCase())) { + if (property.format === undefined) { + property.format = {}; + } + + property.format.label = parsedLabels[property.name.toLowerCase()]; + } + } + } + + if (widths) { + const parsedWidths = new Dictionary(); + for (const w of items(widths)) { + parsedWidths[w.key.toLowerCase()] = w.value; + } + + for (const property of values(properties)) { + if (Object.keys(parsedWidths).includes(property.name.toLowerCase())) { + if (property.format === undefined) { + property.format = {}; + } + + property.format.width = parsedWidths[property.name.toLowerCase()]; + } + } + } + + if (propertiesToInclude) { + const indexes = new Dictionary(); + for (const item of items(propertiesToInclude)) { + indexes[item.value.toLowerCase()] = item.key; + } + + for (const property of values(properties)) { + if (propertiesToInclude.map(x => x.toLowerCase()).includes(property.name.toLowerCase())) { + if (property.format === undefined) { + property.format = {}; + } + + property.format.index = indexes[property.name.toLowerCase()]; + } else { + property.format = { suppressFormat: true }; + } + } + } + + if (propertiesToExclude) { + for (const property of values(properties)) { + if (propertiesToExclude.map(x => x.toLowerCase()).includes(property.name.toLowerCase())) { + property.format = { suppressFormat: true }; + } + } + } + } + + const prevName = model.language.csharp?.name; + if (model.language.csharp) { + model.language.csharp.name = modelNameReplacer ? modelNameRegex ? model.language.csharp.name.replace(modelNameRegex, modelNameReplacer) : modelNameReplacer : model.language.csharp.name; + } + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Changed model-name from ${prevName} to ${model.language.csharp?.name}.` + }); + } + } + + continue; + } + + if (isWhereEnumDirective(directive)) { + const enumNameRegex = getPatternToMatch(directive.where['enum-name']); + const enumValueNameRegex = getPatternToMatch(directive.where['enum-value-name']); + + const enumNameReplacer = directive.set['enum-name']; + const enumValueNameReplacer = directive.set['enum-value-name']; + + let enums = [...state.model.schemas.sealedChoices ?? [], ...state.model.schemas.choices ?? []]; + // let enums = values(state.model.schemas) + // .where(each => each.details.csharp.enum !== undefined) + // .toArray(); + + if (enumNameRegex) { + enums = values(enums) + .where(each => !!`${each.language.csharp?.name}`.match(enumNameRegex)) + .toArray(); + } + + if (enumValueNameRegex) { + const enumsValues = values(enums) + .selectMany(each => each.language.csharp?.enum ? each.language.csharp.enum.values : []) + .where(each => !!`${(each).name}`.match(enumValueNameRegex)) + .toArray(); + for (const enumValue of values(enumsValues)) { + const prevName = (enumValue).name; + (enumValue).name = enumValueNameReplacer ? enumNameRegex ? (enumValue).name.replace(enumValueNameRegex, enumValueNameReplacer) : enumValueNameReplacer : prevName; + if (enumValueNameRegex) { + const enumNames = values(enums) + .select(each => each.language.csharp?.name) + .toArray(); + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Changed enum-value-name from ${prevName} to ${(enumValue).name}. Enum: ${JSON.stringify(enumNames, null, 2)}` + }); + } + } + } else { + for (const each of values(enums)) { + const prevName = each.language.csharp?.name ?? ''; + if (each.language.csharp) { + each.language.csharp.name = enumNameReplacer ? enumNameRegex ? each.language.csharp.name.replace(enumNameRegex, enumNameReplacer) : enumNameReplacer : prevName; + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Changed enum-name from ${prevName} to ${each.language.csharp?.name}.` + }); + } + } + } + + continue; + } + + + if (isRemoveCommandDirective(directive)) { + const selectType = directive.select; + const subjectRegex = getPatternToMatch(directive.where.subject); + const subjectPrefixRegex = getPatternToMatch(directive.where['subject-prefix']); + const verbRegex = getPatternToMatch(directive.where.verb); + const variantRegex = getPatternToMatch(directive.where.variant); + const parameterRegex = getPatternToMatch(directive.where['parameter-name']); + + if (subjectRegex || subjectPrefixRegex || verbRegex || variantRegex || (parameterRegex && selectType === 'command')) { + // select all operations first then reduce by finding the intersection with selectors + let operationsToRemoveKeys = new Set(items(state.model.commands.operations) + .select(operation => operation.key) + .toArray()); + + if (subjectRegex) { + operationsToRemoveKeys = new Set(items(state.model.commands.operations) + .where(operation => !!`${operation.value.details.csharp.subject}`.match(subjectRegex) && operationsToRemoveKeys.has(operation.key)) + .select(operation => operation.key) + .toArray()); + } + + if (subjectPrefixRegex && operationsToRemoveKeys.size > 0) { + operationsToRemoveKeys = new Set(items(state.model.commands.operations) + .where(operation => !!`${operation.value.details.csharp.subjectPrefix}`.match(subjectPrefixRegex) && operationsToRemoveKeys.has(operation.key)) + .select(operation => operation.key) + .toArray()); + } + + if (verbRegex && operationsToRemoveKeys.size > 0) { + operationsToRemoveKeys = new Set(items(state.model.commands.operations) + .where(operation => !!`${operation.value.details.csharp.verb}`.match(verbRegex) && operationsToRemoveKeys.has(operation.key)) + .select(operation => operation.key) + .toArray()); + } + + if (variantRegex && operationsToRemoveKeys.size > 0) { + operationsToRemoveKeys = new Set(items(state.model.commands.operations) + .where(operation => !!`${operation.value.details.csharp.name}`.match(variantRegex) && operationsToRemoveKeys.has(operation.key)) + .select(operation => operation.key) + .toArray()); + } + + if (parameterRegex && selectType === 'command' && operationsToRemoveKeys.size > 0) { + operationsToRemoveKeys = new Set(items(state.model.commands.operations) + .where(operation => values(allVirtualParameters(operation.value.details.csharp.virtualParameters)) + .any(parameter => !!`${parameter.name}`.match(parameterRegex))) + .where(operation => operationsToRemoveKeys.has(operation.key)) + .select(operation => operation.key) + .toArray()); + } + + for (const key of values(operationsToRemoveKeys)) { + const operationInfo = state.model.commands.operations[key].details.csharp; + state.message({ + Channel: Channel.Debug, Text: `[DIRECTIVE] Removed command ${operationInfo.verb}-${operationInfo.subjectPrefix}${operationInfo.subject}${operationInfo.name ? `_${operationInfo.name}` : ''}` + }); + + delete state.model.commands.operations[key]; + } + } + + continue; + } + } + + const operationIdentities = new Set(); + for (const operation of values(state.model.commands.operations)) { + const details = operation.details.csharp; + + let fname = `${details.verb} -${details.subject} -${details.name} `; + let n = 1; + + while (operationIdentities.has(fname)) { + details.name = pascalCase(`${details.name.replace(/\d*$/g, '')} ${n++}`); + fname = pascalCase(`${details.verb} -${details.subject} -${details.name}`); + } + operationIdentities.add(fname); + } + + return state.model; +} + +export async function applyModifiersV2(service: Host) { + // dolauli implement directives + const allDirectives = await service.GetValue('directive'); + directives = values(allDirectives) + // .select(directive => directive) + .where(directive => isWhereCommandDirective(directive) || isWhereModelDirective(directive) || isWhereEnumDirective(directive) || isRemoveCommandDirective(directive)) + .toArray(); + + const state = await new NewModelState(service).init(); + const result = await tweakModel(state); + + await service.WriteFile('code-model-v4-modifiers-v2.yaml', serialize(result), undefined, 'code-model-v4'); +} diff --git a/powershell/plugins/plugin-tweak-model.ts b/powershell/plugins/plugin-tweak-model.ts index cede952c758..9f94a288b98 100644 --- a/powershell/plugins/plugin-tweak-model.ts +++ b/powershell/plugins/plugin-tweak-model.ts @@ -110,7 +110,7 @@ async function tweakModelV2(state: State): Promise { const newProp = new Property(name, param.language.default.description, param.schema); newProp.required = false; newProp.readOnly = false; - newProp.serializedName = name; + newProp.serializedName = param.language.default.serializedName; universalId.properties.push(newProp); } } @@ -172,6 +172,8 @@ async function tweakModelV2(state: State): Promise { // properties with an enum single value are constants // add the constant value property.language.default.constantValue = choiceSchema.choices[0].value; + + // xichen: Do we need skip? } } } diff --git a/powershell/utils/schema.ts b/powershell/utils/schema.ts index 583fb10fad7..c540ac8772d 100644 --- a/powershell/utils/schema.ts +++ b/powershell/utils/schema.ts @@ -9,6 +9,9 @@ import { DeepPartial, } from '@azure-tools/codegen'; import { Dictionary, values } from '@azure-tools/linq'; import { uid } from './uid'; import { Schema, ObjectSchema, Property, SchemaType } from '@azure-tools/codemodel'; +import { NewEnhancedTypeDeclaration } from '../llcsharp/schema/extended-type-declaration'; +import { NewModelClass } from '../llcsharp/model/model-class'; +import { NewModelInterface } from '../llcsharp/model/interface'; export interface PropertyDetails extends ImplementationDetails { required: boolean; @@ -93,6 +96,15 @@ export interface SchemaDetails extends ImplementationDetails { discriminatorValue?: string; suppressFormat?: boolean; + + typeDeclaration?: NewEnhancedTypeDeclaration; + classImplementation?: NewModelClass; + interfaceImplementation?: NewModelInterface; + internalInterfaceImplementation?: NewModelInterface; + interfaceName?: string; + internalInterfaceName?: string; + fullInternalInterfaceName?: string; + fullname?: string; } // export class Schema extends Extensions implements Schema { diff --git a/tests-upgrade/Configuration.json b/tests-upgrade/Configuration.json index 02c76ec53fe..6ad6ceb5e7f 100644 --- a/tests-upgrade/Configuration.json +++ b/tests-upgrade/Configuration.json @@ -9,7 +9,11 @@ "basic-response-multioperation", "basic-spec-required", "basic-spec-root", - "datamodels-datatypes-array" + "datamodels-datatypes-array", + "component-param", + "component-multiparam", + "component-param-localremote", + "component-param-inbody" ], "BlackList": [ "basic-get-querystr",