powershell/plugins/plugin-tweak-m4-model.ts (164 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, CodeModel, DictionarySchema, getAllProperties, HttpHeader, ObjectSchema, Property, Schema, SchemaType } from '@autorest/codemodel'; import { serialize } from '@azure-tools/codegen'; import { PwshModel } from '../utils/PwshModel'; import { ModelState } from '../utils/model-state'; import { StatusCodes } from '../utils/http-definitions'; import { items, values, keys, Dictionary, length } from '@azure-tools/linq'; import { sortPathParameters } from '../utils/sort-parameters'; import { AutorestExtensionHost as Host } from '@autorest/extension-base'; type State = ModelState<PwshModel>; export let directives: Array<any> = []; export async function tweakModelForTsp(state: State): Promise<PwshModel> { const allDirectives = await state.service.getValue<any>('directive'); directives = values(allDirectives).toArray(); return await tweakModel(state); } async function tweakModel(state: State): Promise<PwshModel> { const model = state.model; sortParameters(model); addResponseHeaderSchema(model); addDictionaryApiVersion(model); removeM4DefaultDescription(model); removeExceptionResponse(model); handleNoinlineDirective(state); return model; } //remove error responses except default function removeExceptionResponse(model: CodeModel) { model.operationGroups.forEach(group => { group.operations?.forEach(operation => { operation.exceptions = operation.exceptions?.filter(exception => exception.protocol.http?.statusCodes[0] === 'default'); }); }); } //sort path parameters to follow the order in path for each operation function sortParameters(model: CodeModel) { model.operationGroups.forEach(group => { group.operations?.forEach(operation => { operation.parameters = sortPathParameters(operation.requests?.[0].protocol.http?.path, operation.parameters); }); }); } function handleNoinlineDirective(state: State) { let inlineModels: Array<string> = []; for (const directive of directives.filter(each => each['no-inline'])) { inlineModels = inlineModels.concat(<ConcatArray<string>>values(directive['no-inline']).toArray()); } for (const model of state.model.schemas.objects || []) { if (inlineModels.includes(model.language.default.name)) { model.language.default['skip-inline'] = true; } } } function addResponseHeaderSchema(model: CodeModel) { // In remodeler, each operations response headers will has its own scheam. Each header will be schema's property. // But in m4, if 'schema' is not explicitly defined, even 'headers' is specified, there won't be a schema for headers. // To keep backward compatiable, we will create headers schema here model.operationGroups.forEach((group) => { group.operations?.forEach((op) => { if (!op.responses) { return; } op.responses.forEach((resp) => { if ((<any>resp).schema) { return; } const headers = <Array<HttpHeader>>resp.protocol.http?.headers; if (headers === undefined) { return; } const responseCode = resp.protocol.http?.statusCodes?.[0]; if (responseCode === undefined) { return; } // Follow naming pattern in m3 const code = ((<any>StatusCodes)[responseCode] || '') || responseCode; const schemaName = `${group.language.default.name}_${op.language.default.name} ${code} ResponseHeaders`; const newSchema = model.schemas.objects?.find((schema) => schema.language.default.name === schemaName) || new ObjectSchema(schemaName, ''); newSchema.language.default.isHeaderModel = true; if (!model.schemas.objects) { model.schemas.objects = []; } model.schemas.objects.push(newSchema); headers.forEach((head) => { // We lost description and x-ms-client-name in m4. So newProp's description is empty and use header as serializedName const newProp = new Property(head.header, '', head.schema, { readOnly: false, required: false, serializedName: head.header }); newProp.language.default.HeaderProperty = 'Header'; if (!newSchema.properties) { newSchema.properties = []; } newSchema.properties.push(newProp); }); // Set response header use new schema resp.language.default.headerSchema = newSchema; }); }); }); } function addDictionaryApiVersion(model: CodeModel) { model.schemas.dictionaries?.forEach((schema) => { if (schema.apiVersions) { return; } if (schema.elementType && schema.elementType.apiVersions) { schema.apiVersions = JSON.parse(JSON.stringify(schema.elementType.apiVersions)); } }); // If we cannot find api version from element type, try to get it from object schema who refers the dict or any. model.schemas.objects?.forEach((schema) => { if (!schema.apiVersions) { return; } for (const prop of getAllProperties(schema)) { if (prop.schema.type !== SchemaType.Dictionary || prop.schema.apiVersions) { continue; } prop.schema.apiVersions = JSON.parse(JSON.stringify(schema.apiVersions)); } if (schema.parents) { for (const parent of (schema.parents?.all || [])) { if (parent.type !== SchemaType.Dictionary || parent.apiVersions) { continue; } // for object which both contains properties and additional properties, // we need to skip the model generation for redundant dict. parent.language.default.skip = true; } } }); } function removeM4DefaultDescription(model: CodeModel) { // For dictionary and arrya schema and property, if there is no description assigned, m4 will set a default description like: Dictionary of <type> or Array of <type> // To keep same action as m3, we will set it to empty string const visited = new Set<Schema>(); [...model.schemas.objects ?? [], ...model.schemas.dictionaries ?? [], ...model.schemas.arrays ?? []].forEach((schema) => { recursiveRemoveM4DefaultDescription(schema, visited); }); } function recursiveRemoveM4DefaultDescription(schema: Schema, visited: Set<Schema>) { if (visited.has(schema) || (schema.type !== SchemaType.Object && schema.type !== SchemaType.Dictionary && schema.type !== SchemaType.Array)) { return; } // Default description pattern in m4 const defaultDictDescPattern = /Dictionary of <.?>$/; const defaultArrayDescPattern = /Array of .?$/; visited.add(schema); if (schema.type === SchemaType.Dictionary) { const dictSchema = <DictionarySchema>schema; recursiveRemoveM4DefaultDescription(dictSchema.elementType, visited); if (defaultDictDescPattern.test(dictSchema.language.default.description)) { dictSchema.language.default.description = ''; } } else if (schema.type === SchemaType.Array) { const arrSchema = <ArraySchema>schema; recursiveRemoveM4DefaultDescription(arrSchema.elementType, visited); if (defaultArrayDescPattern.test(schema.language.default.description)) { schema.language.default.description = ''; } } else if (schema.type === SchemaType.Object) { const objSchema = <ObjectSchema>schema; for (const prop of getAllProperties(objSchema)) { recursiveRemoveM4DefaultDescription(prop.schema, visited); if (prop.schema.type === SchemaType.Dictionary && (defaultDictDescPattern.test(prop.language.default.description) || defaultArrayDescPattern.test(prop.language.default.description))) { prop.language.default.description = ''; } } } } export async function tweakM4ModelPlugin(service: Host) { const allDirectives = await service.getValue<any>('directive'); directives = values(allDirectives).toArray(); const state = await new ModelState<PwshModel>(service).init(); service.writeFile({ filename: 'code-model-v4-tweakm4codemodel.yaml', content: serialize(await tweakModel(state)), sourceMap: undefined, artifactType: 'code-model-v4' }); }