powershell/sdk/utility.ts (458 lines of code) (raw):

/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { getAllPublicVirtualPropertiesForSdkWithoutInherited, getAllPublicVirtualPropertiesForSdk, VirtualProperty, VirtualProperties } from '../utils/schema'; import { ArraySchema, DictionarySchema, ObjectSchema, Schema, isObjectSchema, SchemaType, isNumberSchema, Parameter, ChoiceSchema, ConstantSchema, SealedChoiceSchema, Operation, NumberSchema } from '@autorest/codemodel'; import { Dictionary, values } from '@azure-tools/linq'; import { type } from 'os'; import { schema } from '@azure-tools/codemodel-v3'; import { pascalCase, camelCase } from '@azure-tools/codegen'; import { FinallyStatement, Is, Method } from '@azure-tools/codegen-csharp'; export class Helper { useDateTimeOffset: boolean; constructor(useDateTimeOffset = false) { this.useDateTimeOffset = useDateTimeOffset; } public HasConstrains(schema: Schema): boolean { if ((<any>schema).minimum !== undefined || (<any>schema).maximum !== undefined || (<any>schema).maxLength !== undefined || (<any>schema).minLength !== undefined || (<any>schema).maxItems !== undefined || (<any>schema).minItems !== undefined || (<any>schema).multipleOf !== undefined || (<any>schema).pattern !== undefined || (<any>schema).uniqueItems !== undefined) { return true; } return false; } public HasConstantProperty(schema: Schema): boolean { const virtualProperties = this.GetAllPublicVirtualProperties(schema.language.default.virtualProperties); return virtualProperties.filter(p => p.required && (p.property.schema.type === SchemaType.Constant || this.IsConstantEnumProperty(p) || (p.property.schema.type === SchemaType.Object && this.HasConstantProperty(p.property.schema)))).length > 0; } public GetCsharpType(schema: Schema): string { let type = <string>schema.type; if (schema.type === SchemaType.Integer || schema.type === SchemaType.Number) { type = type + (<NumberSchema>schema).precision; } const offset = this.useDateTimeOffset ? 'Offset' : ''; const typeMap = new Map<string, string>([ ['integer', 'int'], ['integer32', 'int'], ['integer64', 'long'], ['number32', 'double'], ['number64', 'double'], ['number128', 'decimal'], ['boolean', 'bool'], ['string', 'string'], ['unixtime', 'System.DateTime'], ['credential', 'string'], ['byte-array', 'byte[]'], ['duration', 'System.TimeSpan'], ['uuid', 'System.Guid'], ['date-time', 'System.DateTime' + offset], ['date', 'System.DateTime'], ['binary', 'string'], ['uri', 'string'], ['arm-id', 'string'] ]); if (typeMap.has(type)) { return <string>typeMap.get(type); } return ''; } private isArraySchema(schema: Schema): schema is ArraySchema { return schema.type === SchemaType.Array; } private isDictionarySchema(schema: Schema): schema is DictionarySchema { return schema.type === SchemaType.Dictionary; } private ShouldValidate(schema: Schema): boolean { if (!schema) { return false; } const typesToValidate = new Array<Schema>(); const validatedTypes = new Set<Schema>(); typesToValidate.push(schema); while (typesToValidate.length > 0) { const modelToValidate = typesToValidate.pop(); if (!modelToValidate) { continue; } else { if (validatedTypes.has(modelToValidate)) { continue; } validatedTypes.add(modelToValidate); if (this.isArraySchema(modelToValidate) || this.isDictionarySchema(modelToValidate)) { typesToValidate.push(modelToValidate.elementType); } else if (isObjectSchema(modelToValidate)) { const virtualProperties = modelToValidate.extensions && modelToValidate.extensions['x-ms-azure-resource'] ? getAllPublicVirtualPropertiesForSdkWithoutInherited(modelToValidate.language.default.virtualProperties) : getAllPublicVirtualPropertiesForSdk(modelToValidate.language.default.virtualProperties); values(virtualProperties).where(p => isObjectSchema(p.property.schema)).forEach(cp => typesToValidate.push(cp.property.schema)); if (values(virtualProperties).any(p => (p.required && p.property.schema.type !== SchemaType.Constant && !this.IsConstantEnumProperty(p)) || this.HasConstrains(p.property.schema))) { return true; } } } } return false; } private appendConstraintValidations(valueReference: string, sb: Array<string>, model: Schema) { const schema = <any>model; if (schema.maximum !== undefined) { const rule = schema.exclusiveMaximum ? 'ExclusiveMaximum' : 'InclusiveMaximum'; const cmp = schema.exclusiveMaximum ? '>=' : '>'; sb.push(`if (${valueReference} ${cmp} ${schema.maximum})`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.${rule}, "${valueReference.replace('this.', '')}", ${schema.maximum});`); sb.push('}'); } if (schema.minimum !== undefined) { const rule = schema.exclusiveMinimum ? 'ExclusiveMinimum' : 'InclusiveMinimum'; const cmp = schema.exclusiveMinimum ? '<=' : '<'; sb.push(`if (${valueReference} ${cmp} ${schema.minimum})`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.${rule}, "${valueReference.replace('this.', '')}", ${schema.minimum});`); sb.push('}'); } if (schema.maxItems !== undefined) { sb.push(`if (${valueReference}.Count > ${schema.maxItems})`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.MaxItems, "${valueReference.replace('this.', '')}", ${schema.maxItems});`); sb.push('}'); } if (schema.maxLength !== undefined) { sb.push(`if (${valueReference}.Length > ${schema.maxLength})`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.MaxLength, "${valueReference.replace('this.', '')}", ${schema.maxLength});`); sb.push('}'); } if (schema.minLength !== undefined) { sb.push(`if (${valueReference}.Length < ${schema.minLength})`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.MinLength, "${valueReference.replace('this.', '')}", ${schema.minLength});`); sb.push('}'); } if (schema.minItems !== undefined) { sb.push(`if (${valueReference}.Count < ${schema.minItems})`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.MinItems, "${valueReference.replace('this.', '')}", ${schema.minItems});`); sb.push('}'); } if (schema.multipleOf !== undefined) { sb.push(`if (${valueReference} % ${schema.multipleOf} != 0)`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.MultipleOf, "${valueReference.replace('this.', '')}", ${schema.multipleOf});`); sb.push('}'); } if (schema.pattern !== undefined) { // eslint-disable-next-line const constraintValue = "\"" + schema.pattern.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\""; let condition = `!System.Text.RegularExpressions.Regex.IsMatch(${valueReference}, ${constraintValue})`; if (schema.type === SchemaType.Dictionary) { condition = `!System.Linq.Enumerable.All(${valueReference}.Values, value => System.Text.RegularExpressions.Regex.IsMatch(value, ${constraintValue}))`; } sb.push(`if (${condition})`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.Pattern, "${valueReference.replace('this.', '')}", ${constraintValue});`); sb.push('}'); } if (schema.uniqueItems !== undefined && 'true' === schema.uniqueItems.toString()) { sb.push(`if (${valueReference}.Count != System.Linq.Enumerable.Count(System.Linq.Enumerable.Distinct(${valueReference})))`); sb.push('{'); sb.push(` throw new Microsoft.Rest.ValidationException(Microsoft.Rest.ValidationRules.UniqueItems, "${valueReference.replace('this.', '')}");`); sb.push('}'); } } private isKindOfString(schema: Schema, required = false): boolean { if (schema.type === SchemaType.String) { return true; } else if (schema.type === SchemaType.Constant && (<ConstantSchema>schema).valueType.type === SchemaType.String) { return true; } else if (schema.type === SchemaType.Choice && (<ChoiceSchema>schema).choiceType.type === SchemaType.String) { return true; } else if (schema.type === SchemaType.SealedChoice && (<ChoiceSchema>schema).choiceType.type === SchemaType.String && (schema.extensions?.['x-ms-model-as-string'] === true || (required && (<ChoiceSchema>schema).choices.length === 1))) { return true; } // ToDo: we need to figure how to handle the case when schema type is enum // Skip it since there is a bug in IsKindOfString in the csharp generator // if (schema.type === SchemaType.Choice && (<ChoiceSchema>schema).choiceType.type === SchemaType.String) { // return true; // } // if (schema.type === SchemaType.SealedChoice // && (<ChoiceSchema>schema).choiceType.type === SchemaType.String) { // // currently assume modelAsString true // return true; // } return false; } public PathParameterString(parameter: Parameter, clientPrefix: string): string { if (!['path', 'query'].includes(parameter.protocol.http?.in)) { return ''; } const prefix = parameter.implementation === 'Client' ? `this${clientPrefix}.` : ''; let res = ''; if (this.isKindOfString(parameter.schema, parameter.required)) { res = `${prefix}${parameter.language.default.name}`; } else { let serializationSettings = `this${clientPrefix}.SerializationSettings`; if (this.IsValueType(parameter.schema.type)) { if (parameter.schema.type === SchemaType.Date) { serializationSettings = 'new Microsoft.Rest.Serialization.DateJsonConverter()'; } else if (parameter.schema.type === SchemaType.DateTime && (<any>parameter.schema).format === 'date-time-rfc1123') { serializationSettings = 'new Microsoft.Rest.Serialization.DateTimeRfc1123JsonConverter()'; } else if (parameter.schema.type === SchemaType.Uri) { serializationSettings = 'new Microsoft.Rest.Serialization.Base64UrlJsonConverter()'; } else if (parameter.schema.type === SchemaType.UnixTime) { serializationSettings = 'new Microsoft.Rest.Serialization.UnixTimeJsonConverter()'; } } res = `Microsoft.Rest.Serialization.SafeJsonConvert.SerializeObject(${prefix}${parameter.language.default.name}, ${serializationSettings}).Trim('"')`; } if (parameter.extensions && parameter.extensions['x-ms-skip-url-encoding']) { return res; } else { return `System.Uri.EscapeDataString(${res})`; } } public ValidateType(schema: Schema, scope: any, valueReference: string, isNullable: boolean, indentation = 3): string { const indentationSpaces = ' '.repeat(indentation); const sb = new Array<string>(); if (!scope) { throw new Error('scope is null'); } if (!!schema && isObjectSchema(schema) && this.ShouldValidateChain(schema)) { sb.push(`${valueReference}.Validate();`); } if (this.HasConstrains(schema)) { this.appendConstraintValidations(valueReference, sb, schema); } if (schema && this.isArraySchema(schema) && this.ShouldValidateChain(schema)) { // ToDo: Should try to get a unique name instead of element let elementVar = 'element'; if (valueReference.startsWith(elementVar)) { elementVar = valueReference + '1'; } const innerValidation = this.ValidateType((<ArraySchema>schema).elementType, scope, elementVar, true, 1); if (innerValidation) { sb.push(`foreach (var ${elementVar} in ${valueReference})`); sb.push('{'); innerValidation.split('\r\n').map(str => sb.push(str)); sb.push('}'); } } else if (schema && this.isDictionarySchema(schema) && this.ShouldValidateChain(schema)) { // ToDo: Should try to get a unique name instead of valueElement let valueVar = 'valueElement'; if (valueReference.startsWith(valueVar)) { valueVar = valueReference + '1'; } const innerValidation = this.ValidateType((<DictionarySchema>schema).elementType, scope, valueVar, true, 1); if (innerValidation) { sb.push(`foreach (var ${valueVar} in ${valueReference}.Values)`); sb.push('{'); innerValidation.split('\r\n').map(str => sb.push(str)); sb.push('}'); } } if (sb.length > 0) { if (this.IsValueType(schema.type) && !isNullable) { return sb.map(str => indentationSpaces + str).join('\r\n'); } else { return `${indentationSpaces}if (${valueReference} != null)\r\n${indentationSpaces}{\r\n${sb.map(str => indentationSpaces + ' ' + str).join('\r\n')}\r\n${indentationSpaces}}`; } } return ''; } public ShouldValidateChain(schema: Schema): boolean { if (!schema) { return false; } const typesToValidate = new Array<Schema>(); const validatedTypes = new Set<Schema>(); typesToValidate.push(schema); while (typesToValidate.length > 0) { const modelToValidate = typesToValidate.pop(); if (!modelToValidate) { continue; } else { validatedTypes.add(modelToValidate); if (this.isArraySchema(modelToValidate) || this.isDictionarySchema(modelToValidate)) { typesToValidate.push(modelToValidate.elementType); } else if (isObjectSchema(modelToValidate)) { if (this.ShouldValidate(modelToValidate)) { return true; } if (modelToValidate.parents && (modelToValidate.parents.immediate.length === 1 && !(modelToValidate.extensions && modelToValidate.extensions['x-ms-azure-resource']))) { typesToValidate.push(modelToValidate.parents.immediate[0]); } } } } return false; } public GetDeserializationSettings(schema: Schema, ref: string): string { if (schema.type === SchemaType.Date) { return 'new Microsoft.Rest.Serialization.DateJsonConverter()'; } else if (schema.type === SchemaType.Uri) { return 'new Microsoft.Rest.Serialization.Base64UrlJsonConverter()'; } else if (schema.type === SchemaType.UnixTime) { return 'new Microsoft.Rest.Serialization.UnixTimeJsonConverter()'; } return ref + '.DeserializationSettings'; } public GetSerializationSettings(schema: Schema, ref: string): string { if (schema.type === SchemaType.Date) { return 'new Microsoft.Rest.Serialization.DateJsonConverter()'; } else if (schema.type === SchemaType.Uri) { return 'new Microsoft.Rest.Serialization.Base64UrlJsonConverter()'; } else if (schema.type === SchemaType.UnixTime) { return 'new Microsoft.Rest.Serialization.UnixTimeJsonConverter()'; } return ref + '.SerializationSettings'; } public IsNullCheckRequiredForVirtualProperty(virtualProperty: VirtualProperty): boolean { return !((this.IsValueType(virtualProperty.property.schema.type) || (virtualProperty.property.schema.type === SchemaType.SealedChoice && ((<SealedChoiceSchema>virtualProperty.property.schema).choiceType.type !== SchemaType.String || (virtualProperty.property.schema.extensions && !virtualProperty.property.schema.extensions['x-ms-model-as-string'])))) && (virtualProperty.required || virtualProperty.property.nullable === false)); } public CamelCase(str: string): string { // str = str.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) { // return index === 0 ? word.toLowerCase() : word.toUpperCase(); // }).replace(/\s+/g, ''); return camelCase(str); } public PascalCase(str: string): string { return pascalCase(str); } public GetAllPublicVirtualProperties(virtualProperties?: VirtualProperties): Array<VirtualProperty> { return getAllPublicVirtualPropertiesForSdk(virtualProperties); } public GetAllPublicVirtualPropertiesWithoutInherited(virtualProperties?: VirtualProperties): Array<VirtualProperty> { return getAllPublicVirtualPropertiesForSdkWithoutInherited(virtualProperties); } public NeedsTransformationConverter(object: ObjectSchema): boolean { for (const property of values(object.properties)) { if (property.extensions && property.extensions['x-ms-client-flatten']) { return true; } } return false; } public IsValueType(type: string): boolean { if (['boolean', 'integer', 'number', 'unixtime', 'duration', 'uuid', 'date-time', 'date'].includes(type)) { return true; } return false; } public HandleConstParameters(operation: Operation): string { const result = new Array<string>(); const bodyParameters = operation.requests && operation.requests.length > 0 ? (operation.requests[0].parameters || []).filter(each => each.protocol.http && each.protocol.http.in === 'body' && !(each.extensions && each.extensions['x-ms-client-flatten']) && each.implementation !== 'Client') : []; const nonBodyParameters = operation.parameters ? operation.parameters.filter(each => each.implementation !== 'Client') : []; const parameters = [...nonBodyParameters, ...bodyParameters].filter(each => this.IsConstantEnumParameter(each)); for (const parameter of values(parameters)) { const quote = (<ChoiceSchema>parameter.schema).choiceType.type === SchemaType.String ? '"' : ''; const csharpType = this.GetCsharpType((<ChoiceSchema>(parameter.schema)).choiceType); result.push(` ${csharpType} ${parameter.language.default.name} = ${quote}${(<ChoiceSchema>parameter.schema).choices[0].value}${quote};`); } return result.join('\r\n'); } public IsConstantEnumParameter(parameter: Parameter): boolean { if (!parameter.required) { // const parameters are always required return false; } // skip parameter.schema.type === SchemaType.Constant, since there is a bug if ((parameter.schema.type === SchemaType.SealedChoice && (<SealedChoiceSchema>(parameter.schema)).choices.length === 1) || (parameter.schema.type === SchemaType.Choice && (<ChoiceSchema>(parameter.schema)).choices.length === 1)) { return true; } return false; } public IsConstantParameter(parameter: Parameter): boolean { if (this.IsConstantEnumParameter(parameter)) { return true; } if (parameter.schema.type === SchemaType.Constant) { return true; } return false; } public IsConstantEnumProperty(property: VirtualProperty): boolean { if (!property.required) { // const parameters are always required return false; } // skip parameter.schema.type === SchemaType.Constant, since there is a bug if ((property.property.schema.type === SchemaType.SealedChoice && (<SealedChoiceSchema>(property.property.schema)).choices.length === 1) || (property.property.schema.type === SchemaType.Choice && (<ChoiceSchema>(property.property.schema)).choices.length === 1)) { return true; } return false; } public IsConstantProperty(property: VirtualProperty): boolean { if (this.IsConstantEnumProperty(property)) { return true; } if (property.property.schema.type === SchemaType.Constant) { return true; } return false; } public GetUniqueName(name: string, usedNames: Array<string>): string { let uniqueName = name; let i = 0; while (usedNames.includes(uniqueName)) { uniqueName = `${name}${++i}`; } return uniqueName; } public GetValidCsharpName(name: string): string { let validChars = name.replace(/[^a-zA-Z0-9_]/g, ''); // prepend '_' if the name starts with a digit if (!/^[a-zA-Z_]/.test(validChars)) { validChars = '_' + validChars; } return validChars; } public IsEnum(schema: Schema): boolean { if (schema.type === SchemaType.SealedChoice && (<SealedChoiceSchema>schema).choiceType.type === SchemaType.String && schema.extensions && !schema.extensions['x-ms-model-as-string']) { return true; } return false; } public ConvertToValidMethodGroupKey(key: string): string { return key.replace(/Operations$/, ''); } private isCloudErrorName(name: string): boolean { // This is to work around the fact that some CloudError will be generated as CloudErrorAutoGenerated in m4 const reg = new RegExp('^CloudErrorAutoGenerated\\d{0,1}$'); if (name === 'CloudError' || reg.test(name)) { return true; } return false; } public IsCloudErrorException(operation: Operation): boolean { return (!operation.exceptions || !(<any>operation.exceptions[0]).schema || this.isCloudErrorName((<any>operation.exceptions[0]).schema.language.default.name)); } public PopulateGroupParameters(parameter: Parameter): string { const groupParameter = parameter.language.default.name; const result = Array<string>(); for (const virtualProperty of values(<Array<VirtualProperty>>(parameter.schema.language.default.virtualProperties.owned))) { let type = virtualProperty.property.schema.language.csharp?.fullname || ''; type = (this.IsValueType(virtualProperty.property.schema.type) || (virtualProperty.property.schema.type === SchemaType.SealedChoice && (<ChoiceSchema>virtualProperty.property.schema).choiceType.type === SchemaType.String && virtualProperty.property.schema.extensions && !virtualProperty.property.schema.extensions['x-ms-model-as-string'])) && !virtualProperty.required ? `${type}?` : type; const CamelName = camelCase(virtualProperty.name); result.push(` ${type} ${CamelName} = default(${type});`); result.push(` if (${groupParameter} != null)`); result.push(' {'); result.push(` ${CamelName} = ${groupParameter}.${pascalCase(CamelName)};`); result.push(' }'); } if (result.length > 0) { return result.join('\r\n'); } return ''; } public wrapComments(indentation: string, prefix: string, comments: string): string { const defaultMaximumCommentColumns = 80; if (comments === null || comments === undefined || comments.length === 0) { return ''; } //cannot predict indentation because we cannot get last line generated const length = defaultMaximumCommentColumns - prefix.length - 1; const result = this.lineBreak(comments, length); for (let i = 0; i < result.length; i++) { if (i != 0) { result[i] = prefix + result[i]; } } return result.join('\n' + indentation); } private lineBreak(comments: string, length: number): Array<string> { const splitter = /\r\n|\r|\n/i; const lines = new Array<string>(); for (const line of comments.split(splitter)) { let processedLine = line; while (processedLine.length > 0) { processedLine = processedLine.trim(); const whiteSpacePositions = [...new Array(processedLine.length).keys()].filter(i => /\s/.test(processedLine[i])).concat([processedLine.length]); let preWidthWrapAt = 0; let postWidthWrapAt = 0; for (const index of whiteSpacePositions) { if (index <= length) { preWidthWrapAt = index; } else if (postWidthWrapAt === 0) { postWidthWrapAt = index; } } const wrapAt = preWidthWrapAt != 0 ? preWidthWrapAt : (postWidthWrapAt != 0 ? postWidthWrapAt : processedLine.length); lines.push(processedLine.substring(0, wrapAt)); processedLine = processedLine.substring(wrapAt); } } return lines; } }