powershell/plugins/cs-namer-v2.ts (200 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 } 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 { Project, 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 { PwshModel } from '../utils/PwshModel'; import { ModelState } from '../utils/model-state'; import { SchemaDetails as NewSchemaDetails, getMutability } from '../utils/schema'; type State = ModelState<PwshModel>; 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 setSchemaNames(schemaGroups: Dictionary<Array<Schema>>, azure: boolean, serviceNamespace: string, keepNames: Array<string>, addAPIVersion = false) { const baseNamespace = new Set<string>(); const subNamespace = new Map<string, Set<string>>(); // dolauli need to notice this -- schemas in the namespace of the lowest supported api version // in Azure Mode, we want to always put schemas into the namespace of the lowest supported apiversion. // otherwise, we just want to differentiate with a simple incremental numbering scheme. 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 (addAPIVersion && 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 = getPascalIdentifier(details.name); const apiName = (!thisApiversion) ? '' : getPascalIdentifier(`Api ${thisApiversion}`); const ns = addAPIVersion && !!thisApiversion ? ['.', apiName] : []; let n = 1; while (thisNamespace.has(schemaName) || (keepNames.includes(schemaName) && schema.language.default?.uid !== 'universal-parameter-type')) { schemaName = getPascalIdentifier(`${details.name}_${n++}`); } thisNamespace.add(schemaName); // object types. if (schema.type === SchemaType.Object || schema.type === SchemaType.Dictionary || schema.type === SchemaType.Any) { schema.language.csharp = { ...details, apiversion: thisApiversion, apiname: apiName, interfaceName: 'I' + pascalCase(fixLeadingNumber([...deconstruct(schemaName)])), // objects have an interfaceName internalInterfaceName: 'I' + pascalCase(fixLeadingNumber([...deconstruct(schemaName), 'Internal'])), // objects have an ineternal interfaceName for setting private members. fullInternalInterfaceName: `${pascalCase([serviceNamespace, '.', 'Models', ...ns])}.${'I' + pascalCase(fixLeadingNumber([...deconstruct(schemaName), 'Internal']))}`, name: getPascalIdentifier(schemaName), namespace: pascalCase([serviceNamespace, '.', 'Models', ...ns]), // objects have a namespace fullname: `${pascalCase([serviceNamespace, '.', 'Models', ...ns])}.${getPascalIdentifier(schemaName)}`, }; } else if (schema.type === SchemaType.Choice || schema.type === SchemaType.SealedChoice) { // oh, it's an enum type 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 }; }) } }; } else { schema.language.csharp = <SchemaDetails>{ ...details, interfaceName: '<INVALID_INTERFACE>', internalInterfaceName: '<INVALID_INTERFACE>', name: schemaName, namespace: '<INVALID_NAMESPACE>', fullname: '<INVALID_FULLNAME>' }; // xichen: for invalid namespace case, we won't create model class. So we do not need consider dup case thisNamespace.delete(schemaName); } // name each property in this schema setPropertyNames(schema); // fix enum names 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)) { // In m3, enum.name and enum.value are same. But in m4, enum.name is named by m4. // To keep same action as m3, use enum.value here (<any>value).name = getPascalIdentifier((<any>value).value); } } } } } 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, await state.getValue('fixed-array', false)) : undefined; const headerSchema = response.language.default.headerSchema; const headerTypeDefinition = headerSchema ? resolver.resolveTypeDeclaration(<any>headerSchema, true, state.path('schemas', headerSchema.language.default.name), await state.getValue('fixed-array', false)) : 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(',')}.` }; } } } } export async function nameStuffRight(state: State): Promise<PwshModel> { const resolver = new SchemaDefinitionResolver(await state.getValue('fixed-array', false)); const model = state.model; // set the namespace for the service const serviceNamespace = await state.getValue('namespace', 'Sample.API'); const azure = await state.getValue('azure', false) || await state.getValue('azure-arm', false); const clientName = getPascalIdentifier(model.language.default.name); const addAPIVersion = await state.getValue('add-api-version-in-model-namespace', false); // 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}` }; const universalIdName = `${await state.getValue('service-name')}Identity`; setSchemaNames(<Dictionary<Array<Schema>>><any>model.schemas, azure, serviceNamespace, [universalIdName], addAPIVersion); await setOperationNames(state, resolver); return model; } export async function csnamerV2(service: Host) { // dolauli add names for http operations and schemas //return processCodeModel(nameStuffRight, service, 'csnamer'); //const session = await startSession<PwshModel>(service, {}, codeModelSchema); //const result = tweakModelV2(session); const state = await new ModelState<PwshModel>(service).init(); await service.writeFile({ filename: 'code-model-v4-csnamer-v2.yaml', content: serialize(await nameStuffRight(state)), sourceMap: undefined, artifactType: 'code-model-v4' }); }