powershell/plugins/sdk-cs-namer.ts (270 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 { codeModelSchema, SchemaResponse, CodeModel, Schema, ObjectSchema, GroupSchema, isObjectSchema, SchemaType, GroupProperty, ParameterLocation, Operation, Parameter, VirtualParameter, getAllProperties, ImplementationLocation, OperationGroup, Request, SchemaContext, StringSchema, ChoiceSchema, SealedChoiceSchema, DictionarySchema, ArraySchema } from '@autorest/codemodel';
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';
import { System } from '@azure-tools/codegen-csharp';
import { Channel, AutorestExtensionHost as Host, Session, startSession } from '@autorest/extension-base';
import { SchemaDetails } from '../llcsharp/code-model';
import { SchemaDefinitionResolver } from '../llcsharp/schema/schema-resolver';
import { SdkModel } from '../utils/SdkModel';
import { ModelState } from '../utils/model-state';
import { DeepPartial } from '@azure-tools/codegen';
import { SchemaDetails as NewSchemaDetails, getMutability } from '../utils/schema';
import { Helper } from '../sdk/utility';
import { getEscapedReservedName } from '../utils/code-namer';
type State = ModelState<SdkModel>;
// function setPropertyNames(schema: Schema) {
// // name each property in this schema
// // skip-for-time-being
// if (!isObjectSchema(schema)) {
// return;
// }
// for (const propertySchema of values(schema.properties)) {
// const propertyDetails = propertySchema.language.default;
// const mutability = getMutability(propertySchema);
// propertyDetails.required = propertySchema.required ?? false;
// propertyDetails.readOnly = propertySchema.readOnly ?? false;
// propertyDetails.read = mutability.read;
// propertyDetails.update = mutability.update && !propertyDetails.readOnly;
// propertyDetails.create = mutability.create && !propertyDetails.readOnly;
// const className = schema.language.csharp?.name;
// let pname = getPascalIdentifier(propertyDetails.name);
// if (pname === className) {
// pname = `${pname}Property`;
// }
// if (pname === 'default') {
// pname = '@default';
// }
// propertySchema.language.csharp = {
// ...propertyDetails,
// name: pname // and so are the propertyNmaes
// };
// if (propertyDetails.isNamedStream) {
// propertySchema.language.csharp.namedStreamPropertyName = pascalCase(fixLeadingNumber([...deconstruct(propertyDetails.name), 'filename']));
// }
// }
// }
function csharpForArray(elementType: Schema, helper: Helper, nullable = true): string {
if (elementType.type === SchemaType.Array) {
// recursively generate the csharpForArray
return `System.Collections.Generic.IList<${csharpForArray((<ArraySchema>elementType).elementType, helper)}>`;
} else if (elementType.type === SchemaType.Dictionary) {
return `System.Collections.Generic.IList<${csharpForDict(<DictionarySchema>elementType, helper)}>`;
}
const rawElementType = elementType;
elementType = rawElementType;
if ((rawElementType.type === SchemaType.Choice || rawElementType.type === SchemaType.SealedChoice) && !helper.IsEnum(rawElementType)) {
elementType = (<ChoiceSchema | SealedChoiceSchema>rawElementType).choiceType;
}
let type = helper.GetCsharpType(elementType);
if (elementType.type === 'any') {
type = 'object';
}
const postfix = ((type && type !== 'string' && type !== 'object') || helper.IsEnum(rawElementType)) && nullable ? '?' : '';
return `System.Collections.Generic.IList<${type ? type + postfix :
(helper.IsEnum(rawElementType) && nullable ? rawElementType.language.default.name + '?' : rawElementType.language.default.name)}>`;
}
function csharpForDict(dictSchema: DictionarySchema, helper: Helper): string {
const rawElementType = dictSchema.elementType;
let elementType = rawElementType;
if ((rawElementType.type === SchemaType.Choice || rawElementType.type === SchemaType.SealedChoice) && !helper.IsEnum(rawElementType)) {
elementType = (<ChoiceSchema | SealedChoiceSchema>rawElementType).choiceType;
}
let valueType = helper.GetCsharpType(elementType) ? helper.GetCsharpType(elementType) : (rawElementType.type === SchemaType.Array ? csharpForArray((<ArraySchema>rawElementType).elementType, helper) : rawElementType.language.default.name);
if (rawElementType.type === 'any') {
valueType = 'object';
} else if (rawElementType.type === SchemaType.Dictionary) {
valueType = csharpForDict(<DictionarySchema>rawElementType, helper);
}
if (((helper.GetCsharpType(elementType) && valueType !== 'string') || helper.IsEnum(rawElementType)) && dictSchema.nullableItems != false) {
valueType += '?';
}
return `System.Collections.Generic.IDictionary<string, ${valueType}>`;
}
function setSchemaNames(schemaGroups: Dictionary<Array<Schema>>, azure: boolean, serviceNamespace: string, helper: Helper) {
const baseNamespace = new Set<string>();
const subNamespace = new Map<string, Set<string>>();
for (const group of values(schemaGroups)) {
for (const schema of group) {
if (schema.language.default.skip) {
continue;
}
let thisNamespace = baseNamespace;
let thisApiversion = '';
// create the namespace if required
if (azure) {
const versions = [...values(schema.apiVersions).select(v => v.version)];
if (schema.language.default?.uid !== 'universal-parameter-type') {
if (versions && length(versions) > 0) {
thisApiversion = minimum(versions);
thisNamespace = subNamespace.get(thisApiversion) || new Set<string>();
subNamespace.set(thisApiversion, thisNamespace);
}
}
}
// for each schema, we're going to set the name
// to the suggested name, unless we have collisions
// at which point, we're going to add a number (for now?)
const details = schema.language.default;
let schemaName = details.name;
const apiName = (!thisApiversion) ? '' : getPascalIdentifier(`Api ${thisApiversion}`);
let n = 1;
while (thisNamespace.has(schemaName)) {
schemaName = `${details.name}_${n++}`;
}
thisNamespace.add(schemaName);
// object types.
if (schema.type === SchemaType.Object) {
schema.language.default.name = getEscapedReservedName(schemaName, 'Model');
schema.language.csharp = {
...details,
apiversion: thisApiversion,
apiname: apiName,
name: getEscapedReservedName(schemaName, 'Model'),
namespace: pascalCase([serviceNamespace, '.', 'Models']), // objects have a namespace
fullname: getEscapedReservedName(schemaName, 'Model'),
};
} else if (schema.type === SchemaType.Any) {
schema.language.csharp = {
...details,
apiversion: thisApiversion,
apiname: apiName,
name: schemaName,
fullname: 'object',
};
} else if (schema.type === SchemaType.Array) {
schema.language.csharp = {
...details,
apiversion: thisApiversion,
apiname: apiName,
name: schemaName,
fullname: csharpForArray((<ArraySchema>schema).elementType, helper, (<ArraySchema>schema).nullableItems != false),
};
} else if (schema.type === SchemaType.Choice || schema.type === SchemaType.SealedChoice) {
// oh, it's an enum type
// ToDo: comment out for time being
// const choiceSchema = <ChoiceSchema<StringSchema> | SealedChoiceSchema<StringSchema>>schema;
// schema.language.csharp = <SchemaDetails>{
// ...details,
// interfaceName: 'I' + pascalCase(fixLeadingNumber([...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
// };
// })
// }
// };
const choiceSchema = <ChoiceSchema<StringSchema> | SealedChoiceSchema<StringSchema>>schema;
schema.language.csharp = <SchemaDetails>{
...details,
interfaceName: 'I' + pascalCase(fixLeadingNumber([...deconstruct(schemaName)])),
name: schemaName,
namespace: pascalCase([serviceNamespace, '.', 'Support']),
fullname: choiceSchema.extensions && !choiceSchema.extensions['x-ms-model-as-string'] && choiceSchema.choiceType.type === SchemaType.String ? schemaName : helper.GetCsharpType(choiceSchema.choiceType),
enum: {
...schema.language.default.enum,
name: schemaName,
values: choiceSchema.choices.map(each => {
return {
...each,
name: each.language.default.name,
description: each.language.default.description
};
})
}
};
} else if (schema.type !== SchemaType.Dictionary) {
// here are primitive types
const schemaDetails = <SchemaDetails>{
...details,
name: schemaName,
fullname: helper.GetCsharpType(schema)
};
// add jonconverters for some types
if (schema.type === SchemaType.Date) {
schemaDetails.jsonConverters = schemaDetails.jsonConverters || [];
schemaDetails.jsonConverters.push('Microsoft.Rest.Serialization.DateJsonConverter');
} else if (schema.type === SchemaType.UnixTime) {
schemaDetails.jsonConverters = schemaDetails.jsonConverters || [];
schemaDetails.jsonConverters.push('Microsoft.Rest.Serialization.UnixTimeJsonConverter');
}
schema.language.csharp = schemaDetails;
// xichen: for invalid namespace case, we won't create model class. So we do not need consider dup case
thisNamespace.delete(schemaName);
} else {
// handle dictionary
const rawElementType = (<DictionarySchema>schema).elementType;
let elementType = rawElementType;
if ((rawElementType.type === SchemaType.Choice || rawElementType.type === SchemaType.SealedChoice) && !helper.IsEnum(rawElementType)) {
elementType = (<ChoiceSchema | SealedChoiceSchema>rawElementType).choiceType;
}
let valueType = helper.GetCsharpType(elementType) ? helper.GetCsharpType(elementType) : (rawElementType.type === SchemaType.Array ? csharpForArray((<ArraySchema>rawElementType).elementType, helper): rawElementType.language.default.name);
if (rawElementType.type === 'any') {
valueType = 'object';
}
if (((helper.GetCsharpType(elementType) && valueType !== 'string') || helper.IsEnum(rawElementType)) && (<DictionarySchema>schema).nullableItems != false) {
valueType += '?';
}
schema.language.csharp = {
...details,
apiversion: thisApiversion,
apiname: apiName,
name: schemaName,
fullname: csharpForDict(<DictionarySchema>schema, helper),
};
}
}
}
}
// async function setOperationNames(state: State, resolver: SchemaDefinitionResolver) {
// // keep a list of operation names that we've assigned.
// const operationNames = new Set<string>();
// for (const operationGroup of values(state.model.operationGroups)) {
// for (const operation of values(operationGroup.operations)) {
// const details = operation.language.default;
// // come up with a name
// const oName = getPascalIdentifier(operationGroup.$key + '_' + details.name);
// let i = 1;
// let operationName = oName;
// while (operationNames.has(operationName)) {
// // if we have used that name, try again.
// operationName = `${oName}${i++}`;
// }
// operationNames.add(operationName);
// operation.language.csharp = {
// ...details, // inherit
// name: operationName,
// };
// // parameters are camelCased.
// for (const parameter of values(operation.parameters)) {
// const parameterDetails = parameter.language.default;
// let propName = camelCase(fixLeadingNumber(deconstruct(parameterDetails.serializedName)));
// if (propName === 'default') {
// propName = '@default';
// }
// parameter.language.csharp = {
// ...parameterDetails,
// name: propName
// };
// }
// const responses = [...values(operation.responses), ...values(operation.exceptions)];
// for (const rsp of responses) {
// // per responseCode
// const response = <SchemaResponse>rsp;
// const responseTypeDefinition = response.schema ? resolver.resolveTypeDeclaration(<any>response.schema, true, state) : undefined;
// const headerSchema = response.language.default.headerSchema;
// const headerTypeDefinition = headerSchema ? resolver.resolveTypeDeclaration(<any>headerSchema, true, state.path('schemas', headerSchema.language.default.name)) : undefined;
// let code = (System.Net.HttpStatusCode[response.protocol.http?.statusCodes[0]] ? System.Net.HttpStatusCode[response.protocol.http?.statusCodes[0]].value : (response.protocol.http?.statusCodes[0] || '')).replace('global::System.Net.HttpStatusCode', '');
// let rawValue = code.replace(/\./, '');
// if (response.protocol.http?.statusCodes[0] === 'default' || rawValue === 'default' || '') {
// rawValue = 'any response code not handled elsewhere';
// code = 'default';
// response.language.default.isErrorResponse = true;
// }
// response.language.csharp = {
// ...response.language.default,
// responseType: responseTypeDefinition ? responseTypeDefinition.declaration : '',
// headerType: headerTypeDefinition ? headerTypeDefinition.declaration : '',
// name: (length(response.protocol.http?.mimeTypes) <= 1) ?
// camelCase(fixLeadingNumber(deconstruct(`on ${code}`))) : // the common type (or the only one.)
// camelCase(fixLeadingNumber(deconstruct(`on ${code} ${response.protocol.http?.mimeTypes[0]}`))),
// description: (length(response.protocol.http?.mimeTypes) <= 1) ?
// `a delegate that is called when the remote service returns ${response.protocol.http?.statusCodes[0]} (${rawValue}).` :
// `a delegate that is called when the remote service returns ${response.protocol.http?.statusCodes[0]} (${rawValue}) with a Content-Type matching ${response.protocol.http?.mimeTypes.join(',')}.`
// };
// }
// }
// }
// }
function duplicateLRO(model: SdkModel) {
for (const operationGroup of model.operationGroups) {
for (const operation of operationGroup.operations) {
if (operation.extensions && operation.extensions['x-ms-long-running-operation']) {
const duplicate = new Operation('Begin' + operation.language.default.name, '', operation);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const extensions = Object.assign({}, duplicate.extensions);
delete extensions['x-ms-long-running-operation'];
if (extensions && 'x-ms-examples' in extensions) {
delete extensions['x-ms-examples'];
}
duplicate.extensions = extensions;
duplicate.language.default.original = duplicate.language.default.name;
duplicate.language.default.name = 'Begin' + duplicate.language.default.name;
operationGroup.operations.push(duplicate);
}
}
}
}
const xmsPageable = 'x-ms-pageable';
// nextLineName is required parameter in 'x-ms-pageable' but its value could be null
const defaultNextLinkName = '';
const defaultItemName = 'value';
function getPageClass(operation: Operation, model: SdkModel): string | null {
if (!operation.extensions || !(xmsPageable in operation.extensions)) {
return null;
}
const nextLinkName = operation.extensions[xmsPageable].nextLinkName || defaultNextLinkName;
const itemName = operation.extensions[xmsPageable].itemName || defaultItemName;
const pair = `${nextLinkName} ${itemName}`;
if (!(pair in model.language.default.pageClasses)) {
const className = Object.keys(model.language.default.pageClasses).length > 0 ? `Page${Object.keys(model.language.default.pageClasses).length}` : 'Page';
model.language.default.pageClasses[pair] = className;
}
return model.language.default.pageClasses[pair];
}
function addNextPageOperation(model: SdkModel) {
model.language.default.pageClasses = model.language.default.pageClasses || {};
for (const operationGroup of model.operationGroups) {
for (const operation of operationGroup.operations) {
if (operation.extensions && xmsPageable in operation.extensions) {
operation.language.default.pageable = {
pageType: getPageClass(operation, model),
ipageType: operation.extensions[xmsPageable].nextLinkName ? 'Microsoft.Rest.Azure.IPage' : 'System.Collections.Generic.IEnumerable',
nextLinkName: operation.extensions[xmsPageable].nextLinkName || defaultNextLinkName,
itemName: operation.extensions[xmsPageable].itemName || defaultItemName,
operationName: operation.extensions[xmsPageable].operationName,
nextPageOperation: false,
};
const extensions = Object.assign({}, operation.extensions);
delete extensions[xmsPageable];
if (operation.extensions[xmsPageable].nextLinkName) {
const nextPageOperation = new Operation(operation.extensions[xmsPageable].operationName || `${operation.language.default.name}Next`, '', operation);
nextPageOperation.language.default.pageable = {
pageType: getPageClass(operation, model),
ipageType: operation.extensions[xmsPageable].nextLinkName ? 'Microsoft.Rest.Azure.IPage' : 'System.Collections.Generic.IEnumerable',
nextLinkName: operation.extensions[xmsPageable].nextLinkName || defaultNextLinkName,
itemName: operation.extensions[xmsPageable].itemName || defaultItemName,
operationName: operation.extensions[xmsPageable].operationName || `${operation.language.default.name}Next`,
nextPageOperation: true,
};
nextPageOperation.extensions = extensions;
nextPageOperation.language.default.original = `${operation.language.default.name}`;
// Set operation name, the name initialization in new Operation() doesn't work
nextPageOperation.language.default.name = nextPageOperation.language.default.pageable.operationName || `${nextPageOperation.language.default.pageable.operationName}Next`;
operationGroup.operations.push(nextPageOperation);
}
operation.extensions = extensions;
}
}
}
}
function correctParameterNames(model: SdkModel) {
for (const operationGroup of model.operationGroups) {
for (const operation of operationGroup.operations) {
for (const parameter of values(operation.parameters)) {
// Use suffix 'property' for parameters in parameter group since we use 'property' for schema names
const suffix = parameter.extensions && parameter.extensions['x-ms-parameter-grouping'] ? 'Property' : 'Parameter';
parameter.language.default.name = getEscapedReservedName(parameter.language.default.name, suffix);
}
if (operation.requests) {
// body parameters
for (const parameter of values(operation.requests[0].parameters)) {
parameter.language.default.name = getEscapedReservedName(parameter.language.default.name, 'Parameter');
}
}
}
}
}
async function nameStuffRight(state: State): Promise<SdkModel> {
const useDateTimeOffset = await state.getValue('useDateTimeOffset', false);
const helper = new Helper(useDateTimeOffset);
const model = state.model;
// set the namespace for the service
const serviceNamespace = await state.getValue('namespace', 'Microsoft.Azure.Sample');
const azure = await state.getValue('azure', false) || await state.getValue('azure-arm', false);
const clientName = getPascalIdentifier(model.language.default.name);
// dolauli see model.details.csharp for c# related staff
// set c# client details (name)
model.language.csharp = {
...model.language.default, // copy everything by default
name: clientName,
namespace: serviceNamespace,
fullname: `${serviceNamespace}.${clientName}`
};
setSchemaNames(<Dictionary<Array<Schema>>><any>model.schemas, azure, serviceNamespace, helper);
duplicateLRO(model);
addNextPageOperation(model);
correctParameterNames(model);
return model;
}
export async function csnamerSdk(service: Host) {
const state = await new ModelState<SdkModel>(service).init();
await service.writeFile({ filename: 'sdk-code-model-v4-csnamer.yaml', content: serialize(await nameStuffRight(state)), sourceMap: undefined, artifactType: 'code-model-v4' });
}