packages/autorest.gotest/src/generator/mockTestGenerator.ts (570 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 {
ArraySchema,
ChoiceSchema,
DateTimeSchema,
DictionarySchema,
GroupProperty,
ImplementationLocation,
Metadata,
ObjectSchema,
Parameter,
Schema,
SchemaType,
} from '@autorest/codemodel';
import { BaseCodeGenerator, BaseDataRender } from './baseGenerator';
import { Config } from '../common/constant';
import { ExampleParameter, ExampleValue } from '@autorest/testmodeler/dist/src/core/model';
import { GoExampleModel, GoMockTestDefinitionModel, ParameterOutput } from '../common/model';
import { GoHelper } from '../util/goHelper';
import { Helper } from '@autorest/testmodeler/dist/src/util/helper';
import { generateReturnsInfo, getAPIParametersSig, getClientParametersSig, getParametersSig, getSchemaResponse } from '../util/codegenBridge';
import { isLROOperation, isMultiRespOperation, isPageableOperation, sortParametersByRequired } from '../common/helpers';
import _ = require('lodash');
import { values } from '@azure-tools/linq';
export class MockTestDataRender extends BaseDataRender {
public clientFactoryParams: Array<Parameter>;
public skipPropertyFunc = (exampleValue: ExampleValue): boolean => {
// skip any null value
if (exampleValue.rawValue === null) {
return true;
}
return false;
};
public replaceValueFunc = (rawValue: any, exampleValue: ExampleValue): any => {
return rawValue;
};
public renderData(): void {
const mockTest = <GoMockTestDefinitionModel>this.context.codeModel.testModel.mockTest;
for (const exampleGroup of mockTest.exampleGroups) {
for (const example of <Array<GoExampleModel>>exampleGroup.examples) {
this.fillExampleOutput(example);
}
}
}
protected fillExampleOutput(example: GoExampleModel): void {
const op = example.operation;
example.opName = op.language.go.name;
if (isPageableOperation(op) && !isLROOperation(op)) {
example.opName = `New${example.opName}Pager`;
}
if (isLROOperation(<any>op)) {
example.opName = 'Begin' + example.opName;
example.isLRO = true;
example.pollerType = example.operation.language.go.responseEnv.language.go.name;
} else {
example.isLRO = false;
}
example.isPageable = isPageableOperation(<any>op);
this.skipPropertyFunc = (exampleValue: ExampleValue): boolean => {
// skip any null value
if (exampleValue.rawValue === null) {
return true;
}
return false;
};
this.replaceValueFunc = (rawValue: any): any => {
return rawValue;
};
const factoryGatherAllParamsFlag = this.context.testConfig.getValue(Config.factoryGatherAllParams, true);
if (factoryGatherAllParamsFlag) {
this.clientFactoryParams = this.getAllClientParameters();
} else {
this.clientFactoryParams = this.getCommonClientParameters();
}
const factoryClientParameters = new Array<Parameter>();
for (const clientParam of values(<Array<Parameter>>(example.operationGroup.language.go?.clientParams || []))) {
if (this.clientFactoryParams.filter((cp) => cp.language.go!.name === clientParam.language.go!.name).length > 0) {
continue;
}
factoryClientParameters.push(clientParam);
}
example.methodParametersOutput = this.toParametersOutput(getAPIParametersSig(op), example.methodParameters);
example.clientParametersOutput = this.toParametersOutput(getClientParametersSig(example.operationGroup), example.clientParameters, true);
example.factoryClientParametersOutput = this.toParametersOutput(getParametersSig(factoryClientParameters), example.clientParameters, true);
example.returnInfo = generateReturnsInfo(op, 'op');
const schemaResponse = getSchemaResponse(<any>op);
if (example.isPageable) {
const valueName = op.extensions['x-ms-pageable'].itemName === undefined ? 'value' : op.extensions['x-ms-pageable'].itemName;
for (const property of schemaResponse.schema['properties']) {
if (property.serializedName === valueName) {
example.pageableItemName = property.language.go.name;
if (schemaResponse.schema.language.go.name === property.language.go.name) {
example.pageableItemName = `${property.language.go.name}.${example.pageableItemName}`;
}
break;
}
}
}
example.checkResponse =
schemaResponse !== undefined && schemaResponse.protocol.http.statusCodes[0] === '200' && example.responses[schemaResponse.protocol.http.statusCodes[0]]?.body !== undefined;
example.isMultiRespOperation = isMultiRespOperation(op);
if (example.checkResponse && this.context.testConfig.getValue(Config.verifyResponse)) {
this.context.importManager.add('encoding/json');
this.context.importManager.add('reflect');
this.skipPropertyFunc = (exampleValue: ExampleValue): boolean => {
// mock-test will remove all NextLink param
// skip any null value
if (exampleValue.language?.go?.name.includes('NextLink') || (exampleValue.rawValue === null && exampleValue.language?.go?.name !== 'ProvisioningState')) {
return true;
}
return false;
};
this.replaceValueFunc = (rawValue: any, exampleValue: ExampleValue): any => {
// mock-test will change all ProvisioningState to Succeeded
if (exampleValue.language?.go?.name === 'ProvisioningState') {
if (exampleValue.schema.type !== SchemaType.SealedChoice || (<ChoiceSchema>exampleValue.schema).choices.filter((choice) => choice.value === 'Succeeded').length > 0) {
return 'Succeeded';
} else {
return (<ChoiceSchema>exampleValue.schema).choices[0].value;
}
}
return rawValue;
};
example.responseOutput = this.exampleValueToString(example.responses[schemaResponse.protocol.http.statusCodes[0]].body, false);
if (isMultiRespOperation(op)) {
example.responseTypePointer = false;
example.responseType = 'Value';
} else {
const responseEnv = op.language.go.responseEnv;
if (responseEnv.language.go?.resultProp.schema.serialization?.xml?.name) {
example.responseTypePointer = !responseEnv.language.go?.resultProp.schema.language.go?.byValue;
example.responseType = responseEnv.language.go?.resultProp.schema.language.go?.name;
if (responseEnv.language.go?.resultProp.schema.isDiscriminator === true) {
example.responseIsDiscriminator = true;
example.responseType = responseEnv.language.go.resultProp.schema.language.go?.discriminatorInterface;
example.responseOutput = `${this.context.packageName}.${responseEnv.language.go.name}{
${example.responseType}: &${example.responseOutput},
}`;
}
} else {
example.responseTypePointer = !responseEnv.language.go?.resultProp.language.go?.byValue;
example.responseType = responseEnv.language.go?.resultProp.language.go?.name;
if (responseEnv.language.go?.resultProp.isDiscriminator === true) {
example.responseIsDiscriminator = true;
example.responseType = responseEnv.language.go.resultProp.schema.language.go?.discriminatorInterface;
example.responseOutput = `${this.context.packageName}.${responseEnv.language.go.name}{
${example.responseType}: &${example.responseOutput},
}`;
}
}
}
}
}
private getCommonClientParameters(): Array<Parameter> {
const paramCount = new Map<string, { uses: number; param: Parameter }>();
let numClients = 0; // track client count since we might skip some
for (const group of this.context.codeModel.operationGroups) {
const clientName = group.language.go!.clientName;
// special cases: some ARM clients always don't contain any parameters (OperationsClient will be depracated in the future)
if (clientName.match(/^OperationsClient$/)) {
continue;
}
numClients++;
if (group.language.go!.clientParams) {
const clientParams = <Array<Parameter>>group.language.go!.clientParams;
for (const clientParam of clientParams) {
let entry = paramCount.get(clientParam.language.go!.name);
if (!entry) {
entry = { uses: 0, param: clientParam };
paramCount.set(clientParam.language.go!.name, entry);
}
++entry.uses;
}
}
}
// for each param, if its usage count is equal to the
// number of clients, then it's common to all clients
const commonClientParams = new Array<Parameter>();
for (const entry of paramCount.values()) {
if (entry.uses === numClients) {
commonClientParams.push(entry.param);
}
}
commonClientParams.sort(sortParametersByRequired);
return commonClientParams;
}
private getAllClientParameters(): Array<Parameter> {
const allClientParams = new Array<Parameter>();
for (const group of this.context.codeModel.operationGroups) {
if (group.language.go!.clientParams) {
const clientParams = <Array<Parameter>>group.language.go!.clientParams;
for (const clientParam of clientParams) {
if (allClientParams.filter((cp) => cp.language.go!.name === clientParam.language.go!.name).length > 0) {
continue;
}
allClientParams.push(clientParam);
}
}
}
allClientParams.sort(sortParametersByRequired);
return allClientParams;
}
// get GO code of all parameters for one operation invoke
protected toParametersOutput(
paramsSig: Array<[string, string, Parameter | GroupProperty]>,
exampleParameters: Array<ExampleParameter>,
isClient = false,
): Array<ParameterOutput> {
return paramsSig.map(([paramName, typeName, parameter]) => {
if (paramName === 'ctx') {
return new ParameterOutput('ctx', 'ctx');
}
return new ParameterOutput(paramName, this.genParameterOutput(paramName, typeName, parameter, exampleParameters, isClient));
});
}
// get GO code of single parameter for one operation invoke
protected genParameterOutput(paramName: string, paramType: string, parameter: Parameter | GroupProperty, exampleParameters: Array<ExampleParameter>, isClient = false): string {
// get corresponding example value of a parameter
const findExampleParameter = (name: string, param: Parameter): string => {
// isPtr need to consider three situation: 1) param is required 2) param is polymorphism 3) param is byValue
const isPolymophismValue = param?.schema?.type === SchemaType.Object && (<ObjectSchema>param.schema).discriminator?.property.isDiscriminator === true;
const isPtr: boolean = isPolymophismValue || !(param.required || param.language.go.byValue === true);
for (const methodParameter of exampleParameters) {
if (this.getLanguageName(methodParameter.parameter) === name) {
// we should judge whether a param or property is ptr or not from outside of exampleValueToString
return this.exampleValueToString(methodParameter.exampleValue, isPtr, elementByValueForParam(param));
}
}
return this.getDefaultValue(param, isPtr, elementByValueForParam(param));
};
if ((<GroupProperty>parameter).originalParameter) {
const group = <GroupProperty>parameter;
const ptr = paramType.startsWith('*') ? '&' : '';
let ret = `${ptr}${this.context.packageName + '.'}${this.getLanguageName(parameter.schema)}{`;
let hasContent = false;
for (const insideParameter of group.originalParameter) {
if (insideParameter.implementation === ImplementationLocation.Client) {
// don't add globals to the per-method options struct
continue;
}
if (this.getLanguageName(insideParameter) === 'ResumeToken') {
// ignore resumeToken param in options
continue;
}
const insideOutput = findExampleParameter(this.getLanguageName(insideParameter), insideParameter);
if (insideOutput) {
ret += `${this.getLanguageName(insideParameter)}: ${insideOutput},\n`;
hasContent = true;
}
}
ret += '}';
if (ptr.length > 0 && !hasContent) {
ret = 'nil';
}
return ret;
}
return findExampleParameter(paramName, parameter);
}
protected getDefaultValue(param: Parameter | ExampleValue, isPtr: boolean, elemByVal = false): string {
if (isPtr) {
return 'nil';
} else {
switch (param.schema.type) {
case SchemaType.Char:
case SchemaType.String:
case SchemaType.Constant:
case SchemaType.Uuid:
return '"<' + Helper.toKebabCase(this.getLanguageName(param)) + '>"';
case SchemaType.Array: {
const elementIsPtr = param.schema.language.go.elementIsPtr && !elemByVal;
const elementPtr = elementIsPtr ? '*' : '';
let elementTypeName = this.getLanguageName((<ArraySchema>param.schema).elementType);
const polymophismName = (<ArraySchema>param.schema).elementType.language.go.discriminatorInterface;
if (polymophismName) {
elementTypeName = polymophismName;
}
return `[]${elementPtr}${GoHelper.addPackage(elementTypeName, this.context.packageName)}{}`;
}
case SchemaType.Dictionary: {
const elementPtr = param.schema.language.go.elementIsPtr ? '*' : '';
const elementTypeName = this.getLanguageName((<DictionarySchema>param.schema).elementType);
return `map[string]${elementPtr}${GoHelper.addPackage(elementTypeName, this.context.packageName)}{}`;
}
case SchemaType.Boolean:
return 'false';
case SchemaType.Integer:
case SchemaType.Number:
return '0';
case SchemaType.Object:
if (isPtr) {
return `&${this.context.packageName + '.'}${this.getLanguageName(param.schema)}{}`;
} else {
return `${this.context.packageName + '.'}${this.getLanguageName(param.schema)}{}`;
}
case SchemaType.AnyObject:
return 'nil';
case SchemaType.Any:
return 'nil';
default:
return '';
}
}
}
protected exampleValueToString(exampleValue: ExampleValue, isPtr: boolean, elemByVal = false, inArray = false): string {
if (exampleValue === null || exampleValue === undefined || exampleValue.isNull) {
return 'nil';
}
const ptr = isPtr ? '&' : '';
if (exampleValue.schema?.type === SchemaType.Array) {
const elementIsPtr = exampleValue.schema.language.go.elementIsPtr && !elemByVal;
const elementPtr = elementIsPtr ? '*' : '';
const schema = <ArraySchema>exampleValue.schema;
const elementIsPolymophism = schema.elementType.language.go.discriminatorInterface !== undefined;
let elementTypeName = this.getLanguageName(schema.elementType);
if (elementIsPolymophism) {
elementTypeName = schema.elementType.language.go.discriminatorInterface;
}
if (exampleValue.elements === undefined) {
const result = `${ptr}[]${elementPtr}${GoHelper.addPackage(elementTypeName, this.context.packageName)}{}`;
return result;
} else {
// for polymorphism element, need to add type name, so pass false for inArray
const result =
`${ptr}[]${elementPtr}${GoHelper.addPackage(elementTypeName, this.context.packageName)}{\n` +
exampleValue.elements.map((x) => this.exampleValueToString(x, elementIsPolymophism || elementIsPtr, false, elementIsPolymophism ? false : true)).join(',\n') +
'}';
return result;
}
} else if (exampleValue.schema?.type === SchemaType.Object) {
if (exampleValue.rawValue === null) {
return 'nil';
}
let output: string;
if (inArray) {
output = '{\n';
} else {
output = `${ptr}${this.context.packageName + '.'}${this.getLanguageName(exampleValue.schema)}{\n`;
}
// object parents' properties will be aggregated to the child
const parentsProps: Array<ExampleValue> = [];
const additionalProps: Array<ExampleValue> = [];
this.aggregateParentsProps(exampleValue.parentsValue, parentsProps, additionalProps);
for (const parentsProp of parentsProps) {
const isPolymophismValue =
parentsProp?.schema?.type === SchemaType.Object &&
((<ObjectSchema>parentsProp.schema).discriminatorValue !== undefined || (<ObjectSchema>parentsProp.schema).discriminator?.property.isDiscriminator === true);
output += `${this.getLanguageName(parentsProp)}: ${this.exampleValueToString(parentsProp, isPolymophismValue || !parentsProp.language.go?.byValue === true)},\n`;
}
// TODO: handle multiple additionalProps
for (const additionalProp of additionalProps) {
output += `AdditionalProperties: ${this.exampleValueToString(additionalProp, false)},\n`;
}
for (const [_, value] of Object.entries(exampleValue.properties || {})) {
if (this.skipPropertyFunc(value)) {
continue;
}
const isPolymophismValue =
value?.schema?.type === SchemaType.Object &&
((<ObjectSchema>value.schema).discriminatorValue !== undefined || (<ObjectSchema>value.schema).discriminator?.property.isDiscriminator === true);
output += `${this.getLanguageName(value)}: ${this.exampleValueToString(value, isPolymophismValue || !value.language.go?.byValue === true)},\n`;
}
output += '}';
return output;
} else if (exampleValue.schema?.type === SchemaType.Dictionary) {
const elementPtr = exampleValue.schema.language.go.elementIsPtr && !elemByVal ? '*' : '';
const elementIsPolymorphism = (<DictionarySchema>exampleValue.schema).elementType.language.go.discriminatorInterface !== undefined;
let elementTypeName = this.getLanguageName((<DictionarySchema>exampleValue.schema).elementType);
if (elementIsPolymorphism) {
elementTypeName = (<DictionarySchema>exampleValue.schema).elementType.language.go.discriminatorInterface;
}
let output = `${ptr}map[string]${elementPtr}${GoHelper.addPackage(elementTypeName, this.context.packageName)}{\n`;
for (const [key, value] of Object.entries(exampleValue.properties || {})) {
// for polymorphism map value, value should be a pointer
output += `${this.getStringValue(key)}: ${this.exampleValueToString(value, exampleValue.schema.language.go.elementIsPtr || elementIsPolymorphism)},\n`;
}
output += '}';
return output;
}
const rawValue = this.getRawValue(exampleValue);
if (rawValue === null) {
return this.getDefaultValue(exampleValue, isPtr);
}
return this.rawValueToString(rawValue, exampleValue.schema, isPtr);
}
protected aggregateParentsProps(exampleValues: Record<string, ExampleValue>, parentsProps: Array<ExampleValue>, additionalProps: Array<ExampleValue>): void {
for (const [_, value] of Object.entries(exampleValues || {})) {
if (value.schema?.type === SchemaType.Object) {
this.aggregateParentsProps(value.parentsValue, parentsProps, additionalProps);
for (const [_, property] of Object.entries(value.properties)) {
if (this.skipPropertyFunc(property)) {
continue;
}
if (
parentsProps.filter((p) => {
return p.language.go.name === property.language.go.name;
}).length > 0
) {
continue;
}
parentsProps.push(property);
}
} else if (value.schema?.type === SchemaType.Dictionary) {
additionalProps.push(value);
} else {
parentsProps.push(value);
}
}
}
protected getRawValue(exampleValue: ExampleValue): void {
exampleValue.rawValue = this.replaceValueFunc(exampleValue.rawValue, exampleValue);
return exampleValue.rawValue;
}
protected getStringValue(rawValue: string): string {
return Helper.quotedEscapeString(rawValue);
}
protected getNumberValue(rawValue: any): string {
return `${Number(rawValue)}`;
}
protected getBoolValue(rawValue: any): string {
return rawValue.toString();
}
protected rawValueToString(rawValue: any, schema: Schema, isPtr: boolean): string {
let ret = JSON.stringify(rawValue);
const goType = this.getLanguageName(schema);
if (schema.type === SchemaType.Choice) {
if ((<ChoiceSchema>schema).choiceType.type === SchemaType.String) {
try {
const choiceValue = Helper.findChoiceValue(<ChoiceSchema>schema, rawValue);
ret = this.context.packageName + '.' + this.getLanguageName(choiceValue);
} catch (error) {
ret = `${this.context.packageName}.${this.getLanguageName(schema)}(${this.getStringValue(rawValue)})`;
}
} else {
ret = `${this.context.packageName}.${this.getLanguageName(schema)}(${this.getNumberValue(rawValue)})`;
}
} else if (schema.type === SchemaType.SealedChoice) {
const choiceValue = Helper.findChoiceValue(<ChoiceSchema>schema, rawValue);
ret = this.context.packageName + '.' + this.getLanguageName(choiceValue);
} else if (goType === 'string') {
ret = this.getStringValue(rawValue);
} else if (schema.type === SchemaType.ByteArray) {
ret = `[]byte(${this.getStringValue(rawValue)})`;
} else if (['int32', 'int64', 'float32', 'float64'].indexOf(goType) >= 0) {
ret = `${this.getNumberValue(rawValue)}`;
} else if (goType === 'time.Time') {
if (schema.type === SchemaType.UnixTime) {
this.context.importManager.add('time');
ret = `time.Unix(${this.getNumberValue(rawValue)}, 0)`;
} else if (schema.type === SchemaType.Date) {
this.context.importManager.add('time');
ret = `func() time.Time { t, _ := time.Parse("2006-01-02", ${this.getStringValue(rawValue)}); return t}()`;
} else {
this.context.importManager.add('time');
const timeFormat = (<DateTimeSchema>schema).format === 'date-time-rfc1123' ? 'time.RFC1123' : 'time.RFC3339Nano';
ret = `func() time.Time { t, _ := time.Parse(${timeFormat}, ${formatRFC3339Nano(rawValue)}); return t}()`;
}
} else if (goType === 'map[string]any' || goType === 'map[string]interface{}') {
ret = this.objectToString(rawValue);
} else if ((goType === 'any' || goType === 'interface{}') && Array.isArray(rawValue)) {
ret = this.arrayToString(rawValue);
} else if ((goType === 'any' || goType === 'interface{}') && typeof rawValue === 'object') {
ret = this.objectToString(rawValue);
} else if ((goType === 'any' || goType === 'interface{}') && _.isNumber(rawValue)) {
ret = `float64(${this.getNumberValue(rawValue)})`;
} else if ((goType === 'any' || goType === 'interface{}') && _.isString(rawValue)) {
ret = this.getStringValue(rawValue);
} else if (goType === 'bool') {
ret = this.getBoolValue(rawValue);
}
if (isPtr) {
const ptrConverts = {
string: 'Ptr',
bool: 'Ptr',
'time.Time': 'Ptr',
int32: 'Ptr[int32]',
int64: 'Ptr[int64]',
float32: 'Ptr[float32]',
float64: 'Ptr[float64]',
};
if ([SchemaType.Choice, SchemaType.SealedChoice].indexOf(schema.type) >= 0) {
ret = `to.Ptr(${ret})`;
} else if (Object.prototype.hasOwnProperty.call(ptrConverts, goType)) {
ret = `to.${ptrConverts[goType]}(${ret})`;
this.context.importManager.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/to');
} else {
ret = '&' + ret;
}
}
return ret;
}
protected getLanguageName(meta: any): string {
return (<Metadata>meta).language.go.name;
}
protected objectToString(rawValue: any): string {
let ret = 'map[string]any{\n';
for (const [key, value] of Object.entries(rawValue)) {
if (_.isArray(value)) {
ret += `"${key}":`;
ret += this.arrayToString(value);
ret += ',\n';
} else if (_.isObject(value)) {
ret += `"${key}":`;
ret += this.objectToString(value);
ret += ',\n';
} else if (_.isString(value)) {
ret += `"${key}": ${this.getStringValue(value)},\n`;
} else if (value === null) {
ret += `"${key}": nil,\n`;
} else if (_.isNumber(value)) {
ret += `"${key}": float64(${value}),\n`;
} else {
ret += `"${key}": ${value},\n`;
}
}
ret += '}';
return ret;
}
protected arrayToString(rawValue: any): string {
let ret = '[]any{\n';
for (const item of rawValue) {
if (_.isArray(item)) {
ret += this.arrayToString(item);
ret += ',\n';
} else if (_.isObject(item)) {
ret += this.objectToString(item);
ret += ',\n';
} else if (_.isString(item)) {
ret += `${Helper.quotedEscapeString(item)},\n`;
} else if (item === null) {
ret += 'nil,\n';
} else if (_.isNumber(item)) {
ret += `float64(${item}),\n`;
} else {
ret += `${item},\n`;
}
}
ret += '}';
return ret;
}
}
export class MockTestCodeGenerator extends BaseCodeGenerator {
public generateCode(extraParam: Record<string, unknown> = {}): void {
this.renderAndWrite(this.context.codeModel.testModel.mockTest, 'mockTest.go.njk', `${this.getFilePrefix(Config.testFilePrefix)}mock_test.go`, extraParam, {
getParamsValue: (params: Array<ParameterOutput>) => {
return params
.map((p) => {
return p.paramOutput;
})
.join(', ');
},
});
}
}
// returns true if the element type for a parameter should be passed by value
export function elementByValueForParam(param: Parameter): boolean {
// passing nil for array elements in headers, paths, and query params
// isn't very useful as we'd just skip nil entries. so disable it.
if (param.schema.type === SchemaType.Array) {
return param.protocol.http!.in === 'header' || param.protocol.http!.in === 'path' || param.protocol.http!.in === 'query';
}
return false;
}
function formatRFC3339Nano(t: string): string {
const date = new Date(t);
function pad(n: number): string {
return n < 10 ? '0' + n : String(n);
}
function pad3(n: number): string {
if (n < 10) {
return '00' + n;
} else if (n < 100) {
return '0' + n;
} else {
return String(n);
}
}
return Helper.quotedEscapeString(
date.getUTCFullYear() +
'-' +
pad(date.getUTCMonth() + 1) +
'-' +
pad(date.getUTCDate()) +
'T' +
pad(date.getUTCHours()) +
':' +
pad(date.getUTCMinutes()) +
':' +
pad(date.getUTCSeconds()) +
'.' +
pad3(date.getUTCMilliseconds()) +
'Z',
);
}