Initial version of tsp emitter (#1347)

* Init tsp

* Add language node

* Add query, path and global parameters

* Add body request

* Init schema

* Add some workaround

* initial support for enum

* Handle unkown type

* Fixed names, handle x-ms-mutability and fixed some issues

* Fixed a few issues found in module generation

* Fixed a few issues

* Add tsphost

* inital generator base on tsp

* Fixed some minor issues

* Add support for header response

* Union support for enum

* Skip deep clone for some operation properties

* Fixed the issue for void input of request

* Fixed a few issues

* Fixed an issue related to anonymous object

* remove redundant psNamer(state)

* Add support for validation

* upgrade tps to 0.57

* Fixed a lint error
This commit is contained in:
Xiaogang 2024-07-04 09:23:46 +08:00 коммит произвёл GitHub
Родитель cc5cec2eb7
Коммит 1bb7ece315
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
38 изменённых файлов: 5180 добавлений и 584 удалений

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -48,12 +48,12 @@ export class CmdletNamespace extends Namespace {
operation.parameters = operation.parameters.filter(element => element.required === true);
}
const newClass = await new CmdletClass(this, operation, this.state.path('commands', 'operations', index)).init();
const refCopyPropertyNames = ['parameters', 'requests', 'responses', 'exceptions', 'requestMediaTypes'];
if (operation.variant.includes('ViaJsonString')) {
const name = 'JsonString';
operation.details.csharp.name = `${operation.variant}Via${name}`;
operation.callGraph[operation.callGraph.length - 1] = clone(operation.callGraph[operation.callGraph.length - 1]);
operation.callGraph[operation.callGraph.length - 1] = clone(operation.callGraph[operation.callGraph.length - 1], false, undefined, undefined, refCopyPropertyNames);
operation.callGraph[operation.callGraph.length - 1].language.csharp!.name = `${(<any>operation.callGraph[operation.callGraph.length - 1]).language.csharp!.name}ViaJsonString`;
}
if (operation.variant.includes('ViaJsonFilePath')) {
@ -67,7 +67,7 @@ export class CmdletNamespace extends Namespace {
const jsonStringField = new Field('_jsonString', System.String);
newClass.add(jsonStringField);
operation.callGraph[operation.callGraph.length - 1] = clone(operation.callGraph[operation.callGraph.length - 1]);
operation.callGraph[operation.callGraph.length - 1] = clone(operation.callGraph[operation.callGraph.length - 1], false, undefined, undefined, refCopyPropertyNames);
operation.callGraph[operation.callGraph.length - 1].language.csharp!.name = `${(<any>operation.callGraph[operation.callGraph.length - 1]).language.csharp!.name}ViaJsonString`;
}
this.addClass(newClass);

2
powershell/exports.ts Normal file
Просмотреть файл

@ -0,0 +1,2 @@
export * from './utils/pwshModel';
export * from './utils/tsp-generator';

Просмотреть файл

@ -34,6 +34,7 @@ import {
Schema as NewSchema,
SchemaType,
} from '@autorest/codemodel';
import { TspHost } from '../utils/tsp-host';
export type Schema = T.SchemaT<
LanguageDetails<SchemaDetails>,
@ -186,17 +187,22 @@ export class Project extends codeDomProject {
}
constructor(
protected service: Host,
protected service: Host | TspHost,
objectInitializer?: DeepPartial<Project>
) {
super();
this.apply(objectInitializer);
}
public async init(): Promise<this> {
public async init(state?: ModelState<PwshModel>): Promise<this> {
await super.init();
this.state = await new State(this.service).init(this);
if (state) {
this.state.model = state.model;
}
this.schemaDefinitionResolver = new PSSchemaResolver();
this.projectNamespace = this.state.model.language.csharp?.namespace || '';
@ -260,7 +266,7 @@ export class Project extends codeDomProject {
// modelcmdlets are models that we will create cmdlets for.
this.modelCmdlets = [];
let directives: Array<any> = [];
const allDirectives = await this.state.getValue('directive');
const allDirectives = await this.state.service.getValue('directive');
directives = values(<any>allDirectives).toArray();
for (const directive of directives.filter((each) => each['model-cmdlet'])) {
this.modelCmdlets = this.modelCmdlets.concat(

Просмотреть файл

@ -10,6 +10,7 @@ import { Project } from './project';
import { DeepPartial } from '@azure-tools/codegen';
import { PwshModel } from '../utils/PwshModel';
import { ModelState } from '../utils/model-state';
import { TspHost } from '../utils/tsp-host';
export interface GeneratorSettings {
@ -27,7 +28,7 @@ export interface GeneratorSettings {
export class State extends ModelState<PwshModel> {
project!: Project;
public constructor(service: Host, objectInitializer?: DeepPartial<State>) {
public constructor(service: Host | TspHost, objectInitializer?: DeepPartial<State>) {
super(service);
this.apply(objectInitializer);
}

Просмотреть файл

@ -12,11 +12,12 @@ import { Dictionary } from '@azure-tools/linq';
import { DeepPartial } from '@azure-tools/codegen';
import { PwshModel } from '../utils/PwshModel';
import { ModelState } from '../utils/model-state';
import { TspHost } from '../utils/tsp-host';
export class State extends ModelState<PwshModel> {
project!: Project;
public constructor(service: Host, objectInitializer?: DeepPartial<State>) {
public constructor(service: Host | TspHost, objectInitializer?: DeepPartial<State>) {
super(service);
this.apply(objectInitializer);
}

Просмотреть файл

@ -14,6 +14,9 @@ import { ServiceNamespace } from './operation/namespace';
import { SupportNamespace } from './enums/namespace';
import { DeepPartial } from '@azure-tools/codegen';
import { PropertyFormat } from '../utils/schema';
import { TspHost } from '../utils/tsp-host';
import { ModelState } from '../utils/model-state';
import { PwshModel } from '../utils/pwshModel';
export class Project extends codeDomProject {
@ -36,15 +39,18 @@ export class Project extends codeDomProject {
supportJsonInput!: boolean;
formats!: Dictionary<PropertyFormat>;
constructor(protected service: Host, objectInitializer?: DeepPartial<Project>) {
constructor(protected service: Host | TspHost, objectInitializer?: DeepPartial<Project>) {
super();
this.apply(objectInitializer);
}
public async init(): Promise<this> {
public async init(state?: ModelState<PwshModel>): Promise<this> {
await super.init();
this.state = await new State(this.service).init(this);
if (state) {
this.state.model = state.model;
}
this.apifolder = await this.state.getValue('api-folder', '');
this.runtimefolder = await this.state.getValue('runtime-folder', 'runtime');
this.azure = await this.state.getValue('azure', false) || await this.state.getValue('azure-arm', false);

Просмотреть файл

@ -42,7 +42,7 @@
"devDependencies": {
"@types/js-yaml": "3.12.1",
"@types/mocha": "5.2.5",
"@types/node": "12.7.2",
"@types/node": "~20.14.0",
"mocha": "10.2.0",
"@testdeck/mocha": "0.3.3",
"typescript": "~5.1.3",

Просмотреть файл

@ -22,7 +22,7 @@ const locationNames = new Set<string>([
'location',
]);
async function tweakModel(state: State): Promise<PwshModel> {
export async function tweakModel(state: State): Promise<PwshModel> {
const model = state.model;
for (const operation of values(model.commands.operations)) {
for (const parameter of values(operation.parameters)) {
@ -46,5 +46,5 @@ async function tweakModel(state: State): Promise<PwshModel> {
export async function addCompleterV2(service: Host) {
const state = await new ModelState<PwshModel>(service).init();
await service.writeFile({filename: 'code-model-v4-add-azure-completers-v2.yaml', content: serialize(await tweakModel(state)), sourceMap: undefined, artifactType: 'code-model-v4'});
await service.writeFile({ filename: 'code-model-v4-add-azure-completers-v2.yaml', content: serialize(await tweakModel(state)), sourceMap: undefined, artifactType: 'code-model-v4' });
}

Просмотреть файл

@ -237,7 +237,7 @@ async function setOperationNames(state: State, resolver: SchemaDefinitionResolve
}
}
async function nameStuffRight(state: State): Promise<PwshModel> {
export async function nameStuffRight(state: State): Promise<PwshModel> {
const resolver = new SchemaDefinitionResolver();
const model = state.model;
@ -271,6 +271,6 @@ export async function csnamerV2(service: Host) {
//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'});
await service.writeFile({ filename: 'code-model-v4-csnamer-v2.yaml', content: serialize(await nameStuffRight(state)), sourceMap: undefined, artifactType: 'code-model-v4' });
}

Просмотреть файл

@ -8,27 +8,30 @@ import { applyOverrides, copyResources, deserialize, serialize, } from '@azure-t
import { join } from 'path';
import { Model } from '../llcsharp/code-model';
import { Project } from '../llcsharp/project';
import { TspHost } from '../utils/tsp-host';
import { ModelState } from '../utils/model-state';
import { PwshModel } from '../utils/pwshModel';
const resources = `${__dirname}/../../resources`;
export async function llcsharpV2(service: Host) {
export async function llcsharpV2(service: Host | TspHost, state?: ModelState<PwshModel>) {
try {
const project = await new Project(service).init();
const project = await new Project(service).init(state);
const transformOutput = async (input: string) => {
return await project.state.resolveVariables(input);
};
await project.writeFiles(async (fname, content) => service.writeFile({ filename: join(project.apifolder, fname), content: applyOverrides(content, project.overrides), sourceMap: undefined, artifactType: 'source-file-csharp'}));
await project.writeFiles(async (fname, content) => project.state.writeFile(join(project.apifolder, fname), applyOverrides(content, project.overrides), undefined, 'source-file-csharp'));
// recursive copy resources
await copyResources(join(resources, 'runtime', 'csharp', 'client'), async (fname, content) => service.writeFile({ filename: join(project.runtimefolder, fname), content: content, sourceMap: undefined, artifactType: 'source-file-csharp'}), project.overrides);
await copyResources(join(resources, 'runtime', 'csharp', 'pipeline'), async (fname, content) => service.writeFile({ filename: join(project.runtimefolder, fname), content: content, sourceMap: undefined, artifactType: 'source-file-csharp'}), project.overrides, transformOutput);
await copyResources(join(resources, 'runtime', 'csharp', 'client'), async (fname, content) => project.state.writeFile(join(project.runtimefolder, fname), content, undefined, 'source-file-csharp'), project.overrides);
await copyResources(join(resources, 'runtime', 'csharp', 'pipeline'), async (fname, content) => project.state.writeFile(join(project.runtimefolder, fname), content, undefined, 'source-file-csharp'), project.overrides, transformOutput);
// Note:
// we are using the Carbon.Json library, but we don't *really* want to expose that as public members where we don't have to
// and I don't want to make code changes in the source repository, so I can keep merging from upstream as simple as possible.
// so, we're converting as much as possible to internal, and exposing only what must be exposed to make the code compile.
await copyResources(join(resources, 'runtime', 'csharp', 'json'), async (fname, content) => service.writeFile({ filename: join(project.runtimefolder, fname), content: content, sourceMap: undefined, artifactType: 'source-file-csharp'}), {
await copyResources(join(resources, 'runtime', 'csharp', 'json'), async (fname, content) => project.state.writeFile(join(project.runtimefolder, fname), content, undefined, 'source-file-csharp'), {
...project.overrides,
'public': 'internal',
'internal (.*) class JsonNumber': 'public $1 class JsonNumber',
@ -65,11 +68,11 @@ export async function llcsharpV2(service: Host) {
});
if (project.xmlSerialization) {
await copyResources(join(resources, 'runtime', 'csharp', 'xml'), async (fname, content) => service.writeFile({ filename: join(project.runtimefolder, fname), content: content, sourceMap: undefined, artifactType: 'source-file-csharp'}), project.overrides);
await copyResources(join(resources, 'runtime', 'csharp', 'xml'), async (fname, content) => project.state.writeFile(join(project.runtimefolder, fname), content, undefined, 'source-file-csharp'), project.overrides);
}
if (project.azure) {
await copyResources(join(resources, 'runtime', 'csharp.azure'), async (fname, content) => service.writeFile({ filename: join(project.runtimefolder, fname), content: content, sourceMap: undefined, artifactType: 'source-file-csharp'}), project.overrides);
await copyResources(join(resources, 'runtime', 'csharp.azure'), async (fname, content) => project.state.writeFile(join(project.runtimefolder, fname), content, undefined, 'source-file-csharp'), project.overrides);
}
} catch (E) {

Просмотреть файл

@ -336,6 +336,14 @@ function isWhereEnumDirective(it: any): it is WhereEnumDirective {
return false;
}
export async function tweakModelForTsp(state: State): Promise<PwshModel> {
const allDirectives = await state.service.getValue<any>('directive');
directives = values(allDirectives)
.where(directive => isWhereCommandDirective(directive) || isWhereModelDirective(directive) || isWhereEnumDirective(directive) || isRemoveCommandDirective(directive))
.toArray();
return await tweakModel(state);
}
async function tweakModel(state: State): Promise<PwshModel> {
const isAzure = await state.getValue('azure', false) || await state.getValue('azure-arm', false);

Просмотреть файл

@ -409,7 +409,7 @@ function createVirtualParameters(operation: CommandOperation) {
operation.details.default.virtualParameters = virtualParameters;
}
async function createVirtuals(state: State): Promise<PwshModel> {
export async function createVirtuals(state: State): Promise<PwshModel> {
/*
A model class should provide inlined properties for anything in a property called properties

Просмотреть файл

@ -14,7 +14,13 @@ import { AutorestExtensionHost as Host } from '@autorest/extension-base';
type State = ModelState<PwshModel>;
let directives: Array<any> = [];
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;
@ -208,3 +214,4 @@ export async function tweakM4ModelPlugin(service: Host) {
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' });
}

Просмотреть файл

@ -15,7 +15,7 @@ type State = ModelState<PwshModel>;
const xmsPageable = 'x-ms-pageable';
async function tweakModel(state: State): Promise<PwshModel> {
export async function tweakModel(state: State): Promise<PwshModel> {
const model = state.model;
// service.message{ Channel: Channel.Debug, Text: "THIS IS THE AZURE TWEAKER" });
@ -198,5 +198,5 @@ function getSchema(response: Response): Schema {
export async function tweakModelAzurePluginV2(service: Host) {
const state = await new ModelState<PwshModel>(service).init();
await service.writeFile({ filename: 'code-model-v4-tweakcodemodelazure-v2.yaml', content: serialize(await tweakModel(state)), sourceMap: undefined, artifactType: 'code-model-v4'});
await service.writeFile({ filename: 'code-model-v4-tweakcodemodelazure-v2.yaml', content: serialize(await tweakModel(state)), sourceMap: undefined, artifactType: 'code-model-v4' });
}

Просмотреть файл

@ -68,7 +68,7 @@ function dropDuplicatePropertiesInChildSchemas(schema: ObjectSchema, state: Stat
return success;
}
async function tweakModelV2(state: State): Promise<PwshModel> {
export async function tweakModelV2(state: State): Promise<PwshModel> {
const title = pascalCase(fixLeadingNumber(deconstruct(await state.getValue('title', state.model.info.title))));
state.setValue('title', title);
@ -645,6 +645,6 @@ export async function tweakModelPlugin(service: Host) {
//const session = await startSession<PwshModel>(service, {}, codeModelSchema);
const state = await new ModelState<PwshModel>(service).init();
//const result = tweakModelV2(session);
await service.writeFile({ filename: 'code-model-v4-tweakcodemodel-v2.yaml', content: serialize(await tweakModelV2(state)), sourceMap: undefined, artifactType: 'code-model-v4'});
await service.writeFile({ filename: 'code-model-v4-tweakcodemodel-v2.yaml', content: serialize(await tweakModelV2(state)), sourceMap: undefined, artifactType: 'code-model-v4' });
//return processCodeModel(tweakModelV2, service, 'tweakcodemodel-v2');
}

Просмотреть файл

@ -24,6 +24,10 @@ import { generateGitIgnore } from '../generators/gitignore';
import { generateGitAttributes } from '../generators/gitattributes';
import { generateReadme } from '../generators/readme';
import { generateScriptCmdlets } from '../generators/script-cmdlet';
import { TspHost } from '../utils/tsp-host';
import { State } from '../internal/state';
import { ModelState } from '../utils/model-state';
import { PwshModel } from '../utils/pwshModel';
const sourceFileCSharp = 'source-file-csharp';
const resources = `${__dirname}/../../resources`;
@ -99,11 +103,11 @@ async function copyRequiredFiles(project: Project) {
}
}
export async function powershellV2(service: Host) {
const debug = (await service.getValue('debug')) || false;
export async function powershellV2(service: Host | TspHost, state?: ModelState<PwshModel>) {
let debug = false;
try {
const project = await new Project(service).init();
const project = await new Project(service).init(state);
await project.writeFiles(async (filename, content) =>
project.state.writeFile(
@ -113,15 +117,15 @@ export async function powershellV2(service: Host) {
sourceFileCSharp
)
);
await service.protectFiles(project.psd1);
await service.protectFiles(project.readme);
await service.protectFiles(project.customFolder);
await service.protectFiles(project.testFolder);
await service.protectFiles(project.docsFolder);
await service.protectFiles(project.examplesFolder);
await service.protectFiles(project.resourcesFolder);
await service.protectFiles(project.uxFolder);
debug = (await project.state.service.getValue('debug')) || false;
await project.state.protectFiles(project.psd1);
await project.state.protectFiles(project.readme);
await project.state.protectFiles(project.customFolder);
await project.state.protectFiles(project.testFolder);
await project.state.protectFiles(project.docsFolder);
await project.state.protectFiles(project.examplesFolder);
await project.state.protectFiles(project.resourcesFolder);
await project.state.protectFiles(project.uxFolder);
// wait for all the generation to be done
await copyRequiredFiles(project);

Просмотреть файл

@ -47,7 +47,7 @@ export function getDeduplicatedNoun(subjectPrefix: string, subject: string): { s
return { subjectPrefix: pascalCase(finalPrefix), subject: pascalCase(reversedFinalSubject.reverse()) };
}
async function tweakModel(state: State): Promise<PwshModel> {
export async function tweakModel(state: State): Promise<PwshModel> {
// get the value
const isAzure = await state.getValue('azure', false);
// without setting snitize-names, isAzure is applied
@ -209,5 +209,5 @@ export async function namerV2(service: Host) {
//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-psnamer-v2.yaml', content: serialize(await tweakModel(state)), sourceMap: undefined, artifactType: 'code-model-v4'});
await service.writeFile({ filename: 'code-model-v4-psnamer-v2.yaml', content: serialize(await tweakModel(state)), sourceMap: undefined, artifactType: 'code-model-v4' });
}

Просмотреть файл

@ -6,6 +6,7 @@
import { Channel, AutorestExtensionHost as Host, JsonPointerSegments as JsonPath, Mapping, RawSourceMap, Message } from '@autorest/extension-base';
import { safeEval, deserialize, Initializer, DeepPartial } from '@azure-tools/codegen';
import { Dictionary } from '@azure-tools/linq';
import { TspHost, TspHostImpl } from './tsp-host';
export class ModelState<T extends Dictionary<any>> extends Initializer {
public model!: T;
@ -15,12 +16,17 @@ export class ModelState<T extends Dictionary<any>> extends Initializer {
private _debug = false;
private _verbose = false;
public constructor(protected service: Host, objectInitializer?: DeepPartial<ModelState<T>>) {
public constructor(public service: TspHost | Host, objectInitializer?: DeepPartial<ModelState<T>>) {
super();
this.apply(objectInitializer);
}
async init(project?: any) {
if (this.service instanceof TspHostImpl) {
// skip init for tsp
this.initContext(project);
return this;
}
const m = await ModelState.getModel<T>(this.service);
this.model = m.model;
this.documentName = m.filename;
@ -88,8 +94,8 @@ export class ModelState<T extends Dictionary<any>> extends Initializer {
async protectFiles(path: string): Promise<void> {
return this.service.protectFiles(path);
}
writeFile(filename: string, content: string, sourceMap?: Array<Mapping> | RawSourceMap | undefined, artifactType?: string | undefined): void {
return this.service.writeFile({ filename: filename, content: content, sourceMap: sourceMap, artifactType: artifactType});
writeFile(filename: string, content: string, sourceMap?: undefined, artifactType?: string | undefined): void {
return this.service.writeFile({ filename: filename, content: content, sourceMap: sourceMap, artifactType: artifactType });
}
message(message: Message): void {
@ -110,7 +116,7 @@ export class ModelState<T extends Dictionary<any>> extends Initializer {
}
protected errorCount = 0;
protected static async getModel<T>(service: Host) {
protected static async getModel<T>(service: Host | TspHost) {
const files = await service.listInputs();
const filename = files[0];
if (files.length === 0) {

Просмотреть файл

@ -0,0 +1,33 @@
import { PwshModel } from '../utils/PwshModel';
import { createVirtuals } from '../plugins/plugin-create-inline-properties';
import { tweakModelV2 } from '../plugins/plugin-tweak-model';
import { tweakModel as tweakModelAzure } from '../plugins/plugin-tweak-model-azure-v2';
import { Inferrer } from '../plugins/create-commands-v2';
import { nameStuffRight } from '../plugins/cs-namer-v2';
import { tweakModel as psNamer } from '../plugins/ps-namer-v2';
import { llcsharpV2 } from '../plugins/llcsharp-v2';
import { powershellV2 } from '../plugins/powershell-v2';
import { tweakModel } from '../plugins/add-azure-completers-v2';
import { tweakModelForTsp as modifier } from '../plugins/modifiers-v2';
import { tweakModelForTsp as tweakM4Model } from '../plugins/plugin-tweak-m4-model';
import { ModelState } from './model-state';
import { TspHostImpl } from './tsp-host';
import { stat } from 'fs';
import { serialize } from '@azure-tools/codegen';
export async function generatePwshModule(pwshModel: PwshModel, emitterOptions: any) {
const tspService = new TspHostImpl(emitterOptions);
const state = await new ModelState<PwshModel>(tspService);
state.model = pwshModel;
await tweakM4Model(state);
await tweakModelV2(state);
await tweakModelAzure(state);
await (await new Inferrer(state).init()).createCommands();
await createVirtuals(state);
await nameStuffRight(state);
await psNamer(state);
await modifier(state);
await llcsharpV2(tspService, state);
await powershellV2(tspService, state);
return;
}

Просмотреть файл

@ -0,0 +1,88 @@
import { Message } from '@autorest/extension-base';
import { writeFileSync, existsSync, mkdirSync } from 'fs';
import { dirname } from 'path';
export interface TspWriteFileOptions {
/**
* @param filename Name of the file.
*/
filename: string;
/**
* @param content Content of the file.
*/
content: string;
/**
* @param sourceMap Source map that can be used to trace back source position in case of error.
*/
sourceMap?: string;
/**
* @param artifactType Artifact type
*/
artifactType?: string;
}
export interface TspHost {
protectFiles(path: string): Promise<void>;
readFile(filename: string): Promise<string>;
getValue<T>(key: string): Promise<T | undefined>;
listInputs(artifactType?: string): Promise<Array<string>>;
writeFile({ filename, content, sourceMap, artifactType }: TspWriteFileOptions): void;
message(message: Message): void;
UpdateConfigurationFile(filename: string, content: string): void;
GetConfigurationFile(filename: string): Promise<string>;
// WriteFile(filename: string, content: string): void;
// GetValue(key: string): any;
}
export class TspHostImpl implements TspHost {
configurations: Record<string, any>;
constructor(configurations: Record<string, any>) {
this.configurations = configurations;
}
protectFiles(path: string): Promise<void> {
// ToDo by xiaogang, skip this for now
return Promise.resolve();
}
readFile(filename: string): Promise<string> {
// ToDo by xiaogang, skip this for now
return Promise.resolve('');
}
getValue<T>(key: string): Promise<T | undefined> {
return this.configurations[key];
}
listInputs(artifactType?: string): Promise<Array<string>> {
// Shall not be called, so throw an exception
throw new Error('Method listInputs not implemented');
}
writeFile({ filename, content, sourceMap, artifactType }: TspWriteFileOptions): void {
const directoryPath = dirname(filename);
if (!existsSync(directoryPath)) {
mkdirSync(directoryPath, { recursive: true });
}
if (artifactType === 'binary-file') {
writeFileSync(filename, Buffer.from(content, 'base64'));
} else {
writeFileSync(filename, content);
}
}
UpdateConfigurationFile(filename: string, content: string): void {
// Shall not be called
throw new Error('Method UpdateConfigurationFile not implemented');
}
GetConfigurationFile(filename: string): Promise<string> {
throw new Error('Method GetConfigurationFile not implemented');
}
// WriteFile(filename: string, content: string): void {
// throw new Error('Method not implemented');
// }
// GetValue(key: string): any {
// throw new Error('Method not implemented');
// }
message(message: Message): void {
// ToDo by xiaogang, skip this for now
return;
}
}

Просмотреть файл

@ -0,0 +1,14 @@
root: true
env:
es2021: true
node: true
extends:
- eslint:recommended
- plugin:@typescript-eslint/recommended
parser: "@typescript-eslint/parser"
parserOptions:
ecmaVersion: latest
sourceType: module
plugins:
- "@typescript-eslint"
rules: {}

Просмотреть файл

@ -0,0 +1,62 @@
{
"name": "@azure-tools/typespec-powershell",
"version": "0.1.0",
"type": "module",
"main": "dist/src/index.js",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
},
"./testing": {
"types": "./dist/src/testing/index.d.ts",
"default": "./dist/src/testing/index.js"
}
},
"dependencies": {
"@autorest/codemodel": "~4.19.3",
"@autorest/extension-base": "~3.5.2",
"@azure-tools/async-io": "~3.0.0",
"@azure-tools/codegen": "~2.5.276",
"@azure-tools/codegen-csharp": "~3.0.0",
"@azure-tools/codemodel-v3": "~3.1.0",
"@azure-tools/linq": "~3.1.0",
"@azure-tools/tasks": "~3.0.0",
"ejs": "~3.1.8",
"js-yaml": "3.13.1",
"source-map-support": "0.5.13",
"xmlbuilder": "10.1.1"
},
"devDependencies": {
"@typespec/compiler": ">=0.51.0 <1.0.0",
"@testdeck/mocha": "0.3.3",
"@types/js-yaml": "3.12.1",
"@types/mocha": "5.2.5",
"@types/node": "~20.14.0",
"@types/xmlbuilder": "0.0.34",
"@typescript-eslint/eslint-plugin": "~5.62.0",
"@typescript-eslint/parser": "~5.62.0",
"eslint": "~8.12.0",
"mocha": "10.2.0",
"prettier": "^3.0.3",
"typescript": "~5.1.3",
"vitest": "1.2.1"
},
"peerDependencies": {
"@azure-tools/typespec-azure-core": ">=0.37.2 <1.0.0",
"@azure-tools/typespec-client-generator-core": ">=0.37.0 <1.0.0",
"@typespec/compiler": ">=0.51.0 <1.0.0",
"@typespec/http": ">=0.51.0 <1.0.0",
"@typespec/rest": ">=0.51.0 <1.0.0",
"@typespec/versioning": ">=0.51.0 <1.0.0"
},
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"test": "node --test ./dist/test/",
"lint": "eslint src/ test/ --report-unused-disable-directives --max-warnings=0",
"lint:fix": "eslint . --report-unused-disable-directives --fix",
"format": "prettier . --write",
"format:check": "prettier --check ."
}
}

Просмотреть файл

@ -0,0 +1,8 @@
trailingComma: "all"
printWidth: 120
quoteProps: "consistent"
endOfLine: lf
arrowParens: always
plugins:
- "./node_modules/@typespec/prettier-plugin-typespec/dist/index.js"
overrides: [{ "files": "*.tsp", "options": { "parser": "typespec" } }]

Просмотреть файл

@ -0,0 +1,370 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { SdkClient, SdkContext, listOperationsInOperationGroup, listOperationGroups } from "@azure-tools/typespec-client-generator-core";
import { HttpOperation, HttpOperationParameter, HttpOperationRequestBody, getHttpOperation } from "@typespec/http";
import { getDoc, getService, ignoreDiagnostics, Program, Model, Type } from "@typespec/compiler";
import { getServers } from "@typespec/http";
import { join } from "path";
import { PwshModel } from "@autorest/powershell";
// import { CodeModel as PwshModel } from "@autorest/codemodel";
import { constantSchemaForApiVersion, getDefaultService, getSchemaForType, schemaCache, stringSchemaForEnum, numberSchemaForEnum, getSchemaForApiVersion, getEnrichedDefaultApiVersion } from "../utils/modelUtils.js";
import { Info, Language, Schemas, AllSchemaTypes, SchemaType, ArraySchema, StringSchema, Languages } from "@autorest/codemodel";
import { deconstruct, pascalCase, serialize } from "@azure-tools/codegen";
import { PSOptions } from "../types/interfaces.js";
import { Request, ImplementationLocation, OperationGroup, Operation, Parameter, Schema, Protocol, Response, HttpHeader } from "@autorest/codemodel";
import { stat } from "fs";
import { extractPagedMetadataNested } from "../utils/operationUtil.js";
import { parseNextLinkName } from "../utils/operationUtil.js";
import { getLroMetadata } from "@azure-tools/typespec-azure-core";
const GlobalParameter = "global-parameter";
export async function transformPwshModel(
client: SdkClient,
psContext: SdkContext,
emitterOptions: PSOptions
): Promise<PwshModel> {
const model = new PwshModel(client.name);
model.info = getServiceInfo(psContext.program);
model.language.default = getLanguageDefault(psContext.program, emitterOptions);
model.operationGroups = getOperationGroups(psContext.program, client, psContext, model);
model.schemas = getSchemas(psContext.program, client, psContext, model);
return model;
}
function getSchemas(program: Program, client: SdkClient, psContext: SdkContext, model: PwshModel): Schemas {
const schemas = new Schemas();
for (const schema of schemaCache.values()) {
if (schema.type === SchemaType.Any) {
// set name and description for any schema
schema.language.default.name = "any";
schema.language.default.description = "Anything";
schemas["any"] = schemas["any"] || [];
schemas["any"].push(schema);
} else {
if (schema.type === SchemaType.Array && (<any>schema).delayType) {
(<ArraySchema>schema).elementType = getSchemaForType(psContext, (<any>schema).delayType as Type);
(<any>schema).delayType = undefined;
}
schemas.add(schema);
}
}
if (stringSchemaForEnum) {
schemas.add(stringSchemaForEnum);
}
if (numberSchemaForEnum) {
schemas.add(numberSchemaForEnum);
}
if (constantSchemaForApiVersion) {
schemas.add(constantSchemaForApiVersion);
}
return schemas;
}
function getOperationGroups(program: Program, client: SdkClient, psContext: SdkContext, model: PwshModel): OperationGroup[] {
const operationGroups: OperationGroup[] = [];
// list all the operations in the client
const clientOperations = listOperationsInOperationGroup(psContext, client);
const newGroup = new OperationGroup("");
if (clientOperations.length > 0) {
newGroup.language.default.name = newGroup.$key = "";
operationGroups.push(newGroup);
}
for (const clientOperation of clientOperations) {
const op = ignoreDiagnostics(getHttpOperation(program, clientOperation));
if (op.overloads && op.overloads.length > 0) {
continue
}
addOperation(psContext, op, newGroup, model);
// const operationGroup = new OperationGroup(operation.name);
// operationGroup.language.default.name = operation.name;
// operationGroup.language.default.description = operation.description;
// operationGroup.operations.push(operation);
// operationGroups.push(operationGroup);
}
const opGroups = listOperationGroups(psContext, client, true);
for (const operationGroup of opGroups) {
const newGroup = new OperationGroup("");
newGroup.language.default.name = newGroup.$key = operationGroup.type.name;
operationGroups.push(newGroup);
const operations = listOperationsInOperationGroup(
psContext,
operationGroup
);
for (const operation of operations) {
const op = ignoreDiagnostics(getHttpOperation(program, operation));
// ignore overload base operation
if (op.overloads && op.overloads?.length > 0) {
continue;
}
addOperation(psContext, op, newGroup, model);
}
}
return operationGroups;
}
function addOperation(psContext: SdkContext, op: HttpOperation, operationGroup: OperationGroup, model: PwshModel) {
const newOperation = new Operation(pascalCase(op.operation.name), getDoc(psContext.program, op.operation) ?? "");
newOperation.operationId = operationGroup.$key + "_" + pascalCase(op.operation.name);
// Add Api versions
newOperation.apiVersions = newOperation.apiVersions || [];
newOperation.apiVersions.push({ version: getEnrichedDefaultApiVersion(psContext.program, psContext) || "" });
// Add query and path parameters
const parameters = op.parameters.parameters.filter(p => p.type === "path" || p.type === "query");
for (const parameter of parameters) {
const newParameter = createParameter(psContext, parameter, model);
newOperation.parameters = newOperation.parameters || [];
newOperation.parameters.push(newParameter);
}
// Add request
newOperation.requests = newOperation.requests || [];
const newRequest = new Request();
newOperation.requests.push(newRequest);
const headerParameters = op.parameters.parameters.filter(p => p.type === "header");
for (const parameter of headerParameters) {
const newParameter = createParameter(psContext, parameter, model);
newRequest.parameters = newRequest.parameters || [];
newOperation.requests[0].parameters?.push(newParameter);
}
// Add host parameter
const hostParameter = createHostParameter(psContext, model);
newOperation.parameters = newOperation.parameters || [];
newOperation.parameters.push(hostParameter);
// Add request body if it exists
if (op.parameters.body && op.parameters.body.bodyKind === "single" && !(op.parameters.body.type.kind === "Intrinsic" && op.parameters.body.type.name === "void")) {
const newParameter = createBodyParameter(psContext, op.parameters.body, model);
newRequest.parameters = newRequest.parameters || [];
newOperation.requests[0].parameters?.push(newParameter);
}
const httpProtocol = new Protocol();
httpProtocol.method = op.verb;
httpProtocol.path = op.path;
// hard code the media type to json for the time being by xiaogang.
httpProtocol.knownMediaType = "json";
httpProtocol.mediaTypes = ["application/json"];
httpProtocol.uri = "{$host}";
newOperation.requests[0].protocol.http = httpProtocol;
// Add responses include exceptions
addResponses(psContext, op, newOperation, model);
// Add extensions
addExtensions(psContext, op, newOperation, model);
operationGroup.addOperation(newOperation);
}
function addExtensions(psContext: SdkContext, op: HttpOperation, newOperation: Operation, model: PwshModel) {
// Add extensions for pageable
const paged = extractPagedMetadataNested(psContext.program, op.responses[0].type as Model);
if (paged) {
newOperation.extensions = newOperation.extensions || {};
//ToDo: add value if it is specified by xiaogang
newOperation.extensions['x-ms-pageable'] = newOperation.extensions['x-ms-pageable'] || {};
newOperation.extensions['x-ms-pageable']['nextLinkName'] = parseNextLinkName(paged) ?? "nextLink";
newOperation.language.default.paging = newOperation.language.default.paging || {};
newOperation.language.default.paging.nextLinkName = parseNextLinkName(paged) ?? "nextLink";
}
// Add extensions for long running operation
const lro = getLroMetadata(psContext.program, op.operation);
if (lro) {
newOperation.extensions = newOperation.extensions || {};
newOperation.extensions['x-ms-long-running-operation'] = true;
newOperation.extensions['x-ms-long-running-operation-options'] = newOperation.extensions['x-ms-long-running-operation-options'] || {};
newOperation.extensions['x-ms-long-running-operation-options']['final-state-via'] = lro.finalStateVia;
}
}
function addResponses(psContext: SdkContext, op: HttpOperation, newOperation: Operation, model: PwshModel) {
const responses = op.responses;
newOperation.responses = newOperation.responses || [];
newOperation.exceptions = newOperation.exceptions || [];
if (responses) {
for (const response of responses) {
const newResponse = new Response();
// newOperation.responses[response.statusCode] || newOperation.responses.default;
// if (!newResponse) {
// newOperation.responses[response.statusCode] = newResponse;
// }
newResponse.language.default.name = '';
newResponse.language.default.description = response.description || "";
const statusCode = response.statusCode;
// Add schema for response body
if (response.responses[0].body) {
const schema = getSchemaForType(psContext, response.responses[0].body.type);
(<any>newResponse).schema = schema;
}
// Add headers
newResponse.protocol.http = newResponse.protocol.http ?? new Protocol();
if (response.responses[0].headers) {
for (const key in response.responses[0].headers) {
newResponse.protocol.http.headers = newResponse.protocol.http.headers || [];
const header = response.responses[0].headers[key];
const headerSchema = getSchemaForType(psContext, header.type);
const headerResponse = new HttpHeader(key, headerSchema);
headerResponse.language = new Languages();
headerResponse.language.default = new Language();
headerResponse.language.default.description = getDoc(psContext.program, header) || "";
headerResponse.language.default.name = pascalCase(deconstruct(key));
newResponse.protocol.http.headers.push(headerResponse);
}
}
newResponse.protocol.http.statusCodes = statusCode === "*" ? ["default"] : [statusCode];
newResponse.protocol.http.knownMediaType = "json";
newResponse.protocol.http.mediaTypes = ["application/json"];
if (statusCode.startsWith("2")) {
newOperation.responses.push(newResponse);
} else {
newOperation.exceptions.push(newResponse);
}
}
}
}
function createBodyParameter(psContext: SdkContext, parameter: HttpOperationRequestBody, model: PwshModel): Parameter {
const paramSchema = parameter.parameter?.sourceProperty
? getSchemaForType(psContext, parameter.parameter?.sourceProperty?.type)
: getSchemaForType(psContext, parameter.type)
const newParameter = new Parameter(parameter.parameter?.name || "", parameter.parameter ? getDoc(psContext.program, parameter.parameter) || "" : "", paramSchema);
newParameter.protocol.http = newParameter.protocol.http ?? new Protocol();
newParameter.protocol.http.in = "body";
// ToDo, we need to support x-ms-client is specified.
newParameter.implementation = ImplementationLocation.Method;
newParameter.required = !parameter.parameter?.optional;
return newParameter;
}
function createHostParameter(psContext: SdkContext, model: PwshModel): Parameter {
const matchParameters = (model.globalParameters || []).filter(p => p.language.default.serializedName === '$host');
if (matchParameters.length > 0) {
return matchParameters[0];
} else {
const newParameter = new Parameter('$host', "server parameter", new StringSchema("", "")); //getSchemaForType(psContext, "string")
newParameter.language.default.serializedName = '$host';
newParameter.clientDefaultValue = "https://management.azure.com";
newParameter.protocol.http = newParameter.protocol.http ?? new Protocol();
newParameter.protocol.http.in = "uri";
newParameter.implementation = ImplementationLocation.Client;
newParameter.required = true;
newParameter.extensions = newParameter.extensions || {};
newParameter.extensions["x-ms-skip-url-encoding"] = true;
model.globalParameters = model.globalParameters || [];
model.globalParameters.push(newParameter);
return newParameter;
}
}
function createParameter(psContext: SdkContext, parameter: HttpOperationParameter, model: PwshModel): Parameter {
if (parameter.type === "query" && parameter.name === "api-version"
|| parameter.type === "path" && parameter.name === "subscriptionId") {
const matchParameters = (model.globalParameters || []).filter(p => p.language.default.serializedName === parameter.name);
if (matchParameters.length > 0) {
return matchParameters[0];
} else {
const paramSchema = parameter.name === "api-version" ? getSchemaForApiVersion(psContext, parameter.param.type) : (parameter.param.sourceProperty ? getSchemaForType(psContext, parameter.param.sourceProperty) : getSchemaForType(psContext, parameter.param));
const newParameter = new Parameter(pascalCase(deconstruct(parameter.name)), getDoc(psContext.program, parameter.param) || "", paramSchema);
if (newParameter.language.default.name === "ApiVersion") {
//to align with modelerfour
newParameter.language.default.name = "apiVersion";
}
newParameter.language.default.serializedName = parameter.name;
newParameter.protocol.http = newParameter.protocol.http ?? new Protocol();
newParameter.protocol.http.in = parameter.type;
newParameter.implementation = ImplementationLocation.Client;
newParameter.required = !parameter.param.optional;
model.globalParameters = model.globalParameters || [];
model.globalParameters.push(newParameter);
return newParameter;
}
} else {
// always create the parameter
const paramSchema = parameter.param.sourceProperty ? getSchemaForType(psContext, parameter.param.sourceProperty) : getSchemaForType(psContext, parameter.param);
const newParameter = new Parameter(parameter.name, getDoc(psContext.program, parameter.param) || "", paramSchema);
newParameter.language.default.serializedName = parameter.name;
newParameter.protocol.http = newParameter.protocol.http ?? new Protocol();
newParameter.protocol.http.in = parameter.type;
// ToDo, we need to support x-ms-client is specified.
newParameter.implementation = ImplementationLocation.Method;
newParameter.required = !parameter.param.optional;
return newParameter;
}
}
// function addGlobalParameter(parameter: Parameter, model: PwshModel) {
// if ((parameter.protocol?.http?.in == "query" && parameter.language.default.name === "api-version")
// || (parameter.protocol?.http?.in == "path" && parameter.language.default.name === "subscriptionId")) {
// model.globalParameters = model.globalParameters || [];
// model.globalParameters.push(parameter);
// }
// }
function getServiceInfo(program: Program): Info {
const defaultService = getDefaultService(program);
const info = new Info(defaultService?.title || "");
info.description = defaultService && getDoc(program, defaultService.type);
return info;
}
function getLanguageDefault(program: Program, emitterOptions: PSOptions): Language {
const defaultLanguage: Language = {
name:
emitterOptions.packageDetails?.name ??
// Todo: may need to normalize the name
pascalCase(deconstruct(emitterOptions?.title ?? getDefaultService(program)?.title ?? "")),
description: ''
};
return defaultLanguage;
}
// export function transformUrlInfo(dpgContext: SdkContext): UrlInfo | undefined {
// const program = dpgContext.program;
// const serviceNs = getDefaultService(program)?.type;
// let endpoint = undefined;
// const urlParameters: PathParameter[] = [];
// if (serviceNs) {
// const host = getServers(program, serviceNs);
// if (host?.[0]?.url) {
// endpoint = host[0].url;
// }
// if (host && host?.[0] && host?.[0]?.parameters) {
// // Currently we only support one parameter in the servers definition
// for (const key of host[0].parameters.keys()) {
// const property = host?.[0]?.parameters.get(key);
// const type = property?.type;
// if (!property || !type) {
// continue;
// }
// const schema = getSchemaForType(dpgContext, type, {
// usage: [SchemaContext.Exception, SchemaContext.Input],
// needRef: false,
// relevantProperty: property
// });
// urlParameters.push({
// oriName: key,
// name: normalizeName(key, NameType.Parameter, true),
// type: getTypeName(schema),
// description:
// (getDoc(program, property) &&
// getFormattedPropertyDoc(program, property, schema, " ")) ??
// getFormattedPropertyDoc(program, type, schema, " " /* sperator*/),
// value: predictDefaultValue(dpgContext, host?.[0]?.parameters.get(key))
// });
// }
// }
// }
// if (endpoint && urlParameters.length > 0) {
// for (const param of urlParameters) {
// if (param.oriName) {
// const regexp = new RegExp(`{${param.oriName}}`, "g");
// endpoint = endpoint.replace(regexp, `{${param.name}}`);
// }
// }
// }
// // Set the default value if missing endpoint parameter
// if (endpoint == undefined && urlParameters.length === 0) {
// endpoint = "{endpoint}";
// urlParameters.push({
// name: "endpoint",
// type: "string"
// });
// }
// return { endpoint, urlParameters };
// }

Просмотреть файл

@ -0,0 +1,44 @@
import { Program, EmitContext, listServices, emitFile, resolvePath } from "@typespec/compiler";
import { serialize } from '@azure-tools/codegen';
import { createSdkContext } from "@azure-tools/typespec-client-generator-core";
// Following files finally should be imported from @autorest/powershell
// import { ModelState } from "./powershell/utils/model-state.js";
// import { PwshModel } from "./powershell/utils/PwshModel.js";
import { getClients } from "./utils/clientUtils.js";
import { transformPwshModel } from "./convertor/convertor.js";
import { PSOptions } from "./types/interfaces.js";
import { generatePwshModule } from "@autorest/powershell";
export async function $onEmit(context: EmitContext) {
const program: Program = context.program;
// Emitter options from tspconfig.json
// ToDo: need to implement a configuration class for AzPS
const emitterOptions: PSOptions = context.options;
const PsContext = createSdkContext(
context,
"@azure-tools/typespec-powershell"
);
// const pwshModel = new PwshModel("");
// const services = listServices(program);
// if (!context.program.compilerOptions.noEmit) {
// await emitFile(context.program, {
// path: resolvePath(context.emitterOutputDir, "output.txt"),
// content: "Hello world\n" + serialize(program.stateMap(Symbol("typescript-models-" + services[0].type.name + "Client"))),
// });
// }
await generatePwshModel();
async function generatePwshModel() {
const clients = getClients(PsContext);
for (const client of clients) {
const model = await transformPwshModel(client, PsContext, emitterOptions);
await emitFile(context.program, {
path: resolvePath(context.emitterOutputDir, `${client.name}.yaml`),
content: serialize(model),
});
generatePwshModule(model, emitterOptions);
}
}
}

Просмотреть файл

@ -0,0 +1,2 @@
export { $onEmit } from "./emitter.js";
export { $lib } from "./lib.js";

Просмотреть файл

@ -0,0 +1,8 @@
import { createTypeSpecLibrary } from "@typespec/compiler";
export const $lib = createTypeSpecLibrary({
name: "powershell-ts",
diagnostics: {},
});
export const { reportDiagnostic, createDiagnostic } = $lib;

Просмотреть файл

@ -0,0 +1,8 @@
import { resolvePath } from "@typespec/compiler";
import { createTestLibrary, TypeSpecTestLibrary } from "@typespec/compiler/testing";
import { fileURLToPath } from "url";
export const PowershellTsTestLibrary: TypeSpecTestLibrary = createTestLibrary({
name: "powershell-ts",
packageRoot: resolvePath(fileURLToPath(import.meta.url), "../../../../"),
});

Просмотреть файл

@ -0,0 +1,12 @@
export interface PSOptions {
packageDetails?: PackageDetails;
title?: string;
}
export interface PackageDetails {
name: string;
scopeName?: string;
nameWithoutScope?: string;
description?: string;
version?: string;
}

Просмотреть файл

@ -0,0 +1,34 @@
import { SdkClient, SdkContext } from "@azure-tools/typespec-client-generator-core";
import {
Namespace,
getNamespaceFullName,
listServices
} from "@typespec/compiler";
export function getClients(psContext: SdkContext): SdkClient[] {
const services = listServices(psContext.program);
return services.map((service) => {
const clientName = service.type.name + "Client";
return {
kind: "SdkClient",
name: clientName,
service: service.type,
type: service.type,
arm: isArm(service.type),
crossLanguageDefinitionId: `${getNamespaceFullName(
service.type
)}.${clientName}`
};
});
}
function isArm(service: Namespace): boolean {
return service.decorators.some(
(decorator) => decorator.decorator.name === "$armProviderNamespace"
);
}
// export function isRLCMultiEndpoint(dpgContext: SdkContext): boolean {
// return getRLCClients(dpgContext).length > 1;
// }

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,80 @@
import { getAllModels } from "@azure-tools/typespec-client-generator-core";
import {
Namespace,
isGlobalNamespace,
isService,
Operation
} from "@typespec/compiler";
// import { SdkContext } from "./interfaces.js";
import { SdkContext } from "@azure-tools/typespec-client-generator-core";
export function getModelNamespaceName(
dpgContext: SdkContext,
namespace: Namespace
): string[] {
const result: string[] = [];
namespace &&
!isGlobalNamespace(dpgContext.program, namespace) &&
!isService(dpgContext.program, namespace)
? (result.push(...getModelNamespaceName(dpgContext, namespace.namespace!)),
result.push(namespace.name))
: result;
return result;
}
// export function getOperationNamespaceInterfaceName(
// dpgContext: SdkContext,
// operation: Operation
// ): string[] {
// const result: string[] = [];
// if (
// dpgContext.rlcOptions?.hierarchyClient === false &&
// dpgContext.rlcOptions?.enableOperationGroup !== true
// ) {
// return result;
// }
// if (operation.interface) {
// if (
// dpgContext.rlcOptions?.enableOperationGroup === true &&
// dpgContext.rlcOptions?.hierarchyClient === false
// ) {
// result.push(operation.interface.name);
// return result;
// }
// if (
// operation.interface.namespace &&
// !isGlobalNamespace(dpgContext.program, operation.interface.namespace) &&
// !isService(dpgContext.program, operation.interface.namespace)
// ) {
// result.push(
// ...getModelNamespaceName(dpgContext, operation.interface.namespace)
// );
// }
// result.push(operation.interface.name);
// } else if (operation.namespace) {
// !isGlobalNamespace(dpgContext.program, operation.namespace) &&
// !isService(dpgContext.program, operation.namespace)
// ? (result.push(
// ...getModelNamespaceName(dpgContext, operation.namespace.namespace!)
// ),
// result.push(operation.namespace.name))
// : result;
// }
// return result;
// }
export function detectModelConflicts(dpgContext: SdkContext) {
const allModels = getAllModels(dpgContext);
const nameSet = new Set<string>();
for (const model of allModels) {
if (model.name === "") {
continue;
}
if (nameSet.has(model.name)) {
return true;
}
nameSet.add(model.name);
}
return false;
}

Просмотреть файл

@ -0,0 +1,543 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// import {
// NameType,
// Paths,
// ResponseMetadata,
// ResponseTypes,
// getLroLogicalResponseName,
// getResponseTypeName,
// normalizeName
// } from "@azure-tools/rlc-common";
import {
getProjectedName,
ignoreDiagnostics,
Model,
Operation,
Program,
Type
} from "@typespec/compiler";
import {
HttpOperation,
HttpOperationParameter,
HttpOperationResponse,
HttpStatusCodesEntry,
getHttpOperation
} from "@typespec/http";
import {
getLroMetadata,
getPagedResult,
PagedResultMetadata
} from "@azure-tools/typespec-azure-core";
import {
SdkClient,
listOperationGroups,
listOperationsInOperationGroup,
SdkContext
} from "@azure-tools/typespec-client-generator-core";
// import {
// OperationLroDetail,
// OPERATION_LRO_LOW_PRIORITY,
// OPERATION_LRO_HIGH_PRIORITY
// } from "@azure-tools/rlc-common";
// import { isByteOrByteUnion } from "./modelUtils.js";
// import { SdkContext } from "./interfaces.js";
// import { getOperationNamespaceInterfaceName } from "./namespaceUtils.js";
// import { KnownMediaType, knownMediaType } from "./mediaTypes.js";
// Sorts the responses by status code
export function sortedOperationResponses(responses: HttpOperationResponse[]) {
return responses.sort((a, b) => {
if (a.statusCodes === "*") {
return 1;
}
if (b.statusCodes === "*") {
return -1;
}
const aStatus =
typeof a.statusCodes === "number" ? a.statusCodes : a.statusCodes.start;
const bStatus =
typeof b.statusCodes === "number" ? b.statusCodes : b.statusCodes.start;
return aStatus - bStatus;
});
}
/**
* This function computes all the response types error and success
* an operation can end up returning.
*/
// ToDo: get response by xiaogang
// export function getOperationResponseTypes(
// dpgContext: SdkContext,
// operation: HttpOperation
// ): ResponseTypes {
// const returnTypes: ResponseTypes = {
// error: [],
// success: []
// };
// function getResponseType(responses: HttpOperationResponse[]) {
// return responses.map((r) => {
// const statusCode = getOperationStatuscode(r);
// const responseName = getResponseTypeName(
// getOperationGroupName(dpgContext, operation),
// getOperationName(dpgContext.program, operation.operation),
// statusCode
// );
// return responseName;
// });
// }
// if (operation.responses && operation.responses.length) {
// returnTypes.error = getResponseType(
// operation.responses.filter((r) => isDefaultStatusCode(r.statusCodes))
// );
// returnTypes.success = getResponseType(
// operation.responses.filter((r) => isDefinedStatusCode(r.statusCodes))
// );
// }
// return returnTypes;
// }
/**
* Extracts all success or defined status codes for a give operation
*/
export function getOperationSuccessStatus(operation: HttpOperation): string[] {
const responses = operation.responses ?? [];
const status: string[] = [];
for (const response of responses) {
if (isDefinedStatusCode(response.statusCodes)) {
status.push(getOperationStatuscode(response));
}
}
return status;
}
export function getOperationStatuscode(
response: HttpOperationResponse
): string {
const statusCodes = response.statusCodes;
if (statusCodes === "*") {
return "default";
} else if (typeof statusCodes === "number") {
return String(statusCodes);
} else {
// FIXME - this is a hack to get the first status code
// https://github.com/Azure/autorest.typescript/issues/2063
return String(statusCodes.start);
}
}
// export function getOperationGroupName(
// dpgContext: SdkContext,
// route?: HttpOperation
// ): string;
// export function getOperationGroupName(
// dpgContext: SdkContext,
// operation?: Operation
// ): string;
// export function getOperationGroupName(
// dpgContext: SdkContext,
// operationOrRoute?: Operation | HttpOperation
// ) {
// if (!dpgContext.rlcOptions?.enableOperationGroup || !operationOrRoute) {
// return "";
// }
// // If this is a HttpOperation
// if ((operationOrRoute as any).kind !== "Operation") {
// operationOrRoute = (operationOrRoute as HttpOperation).operation;
// }
// const operation = operationOrRoute as Operation;
// const namespaceNames = getOperationNamespaceInterfaceName(
// dpgContext,
// operation
// );
// return namespaceNames
// .map((name) => {
// return normalizeName(name, NameType.Interface, true);
// })
// .join("");
// }
// export function getOperationName(program: Program, operation: Operation) {
// const projectedOperationName = getProjectedName(program, operation, "json");
// return normalizeName(
// projectedOperationName ?? operation.name,
// NameType.Interface,
// true
// );
// }
export function isDefaultStatusCode(statusCodes: HttpStatusCodesEntry) {
return statusCodes === "*";
}
export function isDefinedStatusCode(statusCodes: HttpStatusCodesEntry) {
return statusCodes !== "*";
}
// ToDo: Binary playload by xiaogang
// export function isBinaryPayload(
// dpgContext: SdkContext,
// body: Type,
// contentType: string | string[]
// ) {
// const knownMediaTypes: KnownMediaType[] = (
// Array.isArray(contentType) ? contentType : [contentType]
// ).map((ct) => knownMediaType(ct));
// for (const type of knownMediaTypes) {
// if (type === KnownMediaType.Binary && isByteOrByteUnion(dpgContext, body)) {
// return true;
// }
// }
// return false;
// }
export function isLongRunningOperation(
program: Program,
operation: HttpOperation
) {
return Boolean(getLroMetadata(program, operation.operation));
}
/**
* Return if we have a client-level LRO overloading
* @param pathDictionary
* @returns
*/
// export function getClientLroOverload(pathDictionary: Paths) {
// let lroCounts = 0,
// allowCounts = 0;
// for (const details of Object.values(pathDictionary)) {
// for (const methodDetails of Object.values(details.methods)) {
// const lroDetail = methodDetails[0].operationHelperDetail?.lroDetails;
// if (lroDetail?.isLongRunning) {
// lroCounts++;
// if (!lroDetail.operationLroOverload) {
// return false;
// }
// allowCounts++;
// }
// }
// }
// return Boolean(lroCounts > 0 && lroCounts === allowCounts);
// }
/**
* Check if we have an operation-level overloading
* @param program
* @param operation The operation detail
* @param existingResponseTypes auxilary param for current response types
* @param existingResponses auxilary param for raw response data
* @returns
*/
// export function getOperationLroOverload(
// program: Program,
// operation: HttpOperation,
// existingResponseTypes?: ResponseTypes,
// existingResponses?: ResponseMetadata[]
// ) {
// const metadata = getLroMetadata(program, operation.operation);
// if (!metadata) {
// return false;
// }
// const hasSuccessReturn = existingResponses?.filter((r) =>
// r.statusCode.startsWith("20")
// );
// if (existingResponseTypes?.success || hasSuccessReturn) {
// return true;
// }
// return false;
// }
/**
* Extract the operation LRO details
* @param program
* @param operation Operation detail
* @param responsesTypes Calculated response types
* @param operationGroupName Operation group name
* @returns
*/
// export function extractOperationLroDetail(
// program: Program,
// operation: HttpOperation,
// responsesTypes: ResponseTypes,
// operationGroupName: string
// ): OperationLroDetail {
// let logicalResponseTypes: ResponseTypes | undefined;
// let precedence = OPERATION_LRO_LOW_PRIORITY;
// const operationLroOverload = getOperationLroOverload(
// program,
// operation,
// responsesTypes
// );
// if (operationLroOverload) {
// logicalResponseTypes = {
// error: responsesTypes.error,
// success: [
// getLroLogicalResponseName(
// operationGroupName,
// getOperationName(program, operation.operation)
// )
// ]
// };
// const metadata = getLroMetadata(program, operation.operation);
// precedence =
// metadata?.finalStep &&
// metadata.finalStep.kind === "pollingSuccessProperty" &&
// metadata?.finalStep.target &&
// metadata?.finalStep?.target?.name === "result"
// ? OPERATION_LRO_HIGH_PRIORITY
// : OPERATION_LRO_LOW_PRIORITY;
// }
// return {
// isLongRunning: Boolean(getLroMetadata(program, operation.operation)),
// logicalResponseTypes,
// operationLroOverload,
// precedence
// };
// }
export function hasPollingOperations(
program: Program,
client: SdkClient,
dpgContext: SdkContext
) {
const clientOperations = listOperationsInOperationGroup(dpgContext, client);
for (const clientOp of clientOperations) {
const route = ignoreDiagnostics(getHttpOperation(program, clientOp));
// ignore overload base operation
if (route.overloads && route.overloads?.length > 0) {
continue;
}
if (isLongRunningOperation(program, route)) {
return true;
}
}
const operationGroups = listOperationGroups(dpgContext, client, true);
for (const operationGroup of operationGroups) {
const operations = listOperationsInOperationGroup(
dpgContext,
operationGroup
);
for (const op of operations) {
const route = ignoreDiagnostics(getHttpOperation(program, op));
// ignore overload base operation
if (route.overloads && route.overloads?.length > 0) {
continue;
}
if (isLongRunningOperation(program, route)) {
return true;
}
}
}
return false;
}
export function isPagingOperation(program: Program, operation: HttpOperation) {
for (const response of operation.responses) {
const paged = extractPagedMetadataNested(program, response.type as Model);
if (paged) {
return true;
}
}
return false;
}
export function hasPagingOperations(
program: Program,
client: SdkClient,
dpgContext: SdkContext
) {
const clientOperations = listOperationsInOperationGroup(dpgContext, client);
for (const clientOp of clientOperations) {
const route = ignoreDiagnostics(getHttpOperation(program, clientOp));
// ignore overload base operation
if (route.overloads && route.overloads?.length > 0) {
continue;
}
if (isPagingOperation(program, route)) {
return true;
}
}
const operationGroups = listOperationGroups(dpgContext, client, true);
for (const operationGroup of operationGroups) {
const operations = listOperationsInOperationGroup(
dpgContext,
operationGroup
);
for (const op of operations) {
const route = ignoreDiagnostics(getHttpOperation(program, op));
// ignore overload base operation
if (route.overloads && route.overloads?.length > 0) {
continue;
}
if (isPagingOperation(program, route)) {
return true;
}
}
}
return false;
}
export function extractPagedMetadataNested(
program: Program,
type: Model
): PagedResultMetadata | undefined {
// This only works for `is Page<T>` not `extends Page<T>`.
let paged = getPagedResult(program, type);
if (paged) {
return paged;
}
if (type.baseModel) {
paged = getPagedResult(program, type.baseModel);
}
if (paged) {
return paged;
}
const templateArguments = type.templateMapper?.args;
if (templateArguments) {
for (const argument of templateArguments) {
const modelArgument = argument as Model;
if (modelArgument) {
paged = extractPagedMetadataNested(program, modelArgument);
if (paged) {
return paged;
}
}
}
}
return paged;
}
export function getSpecialSerializeInfo(
paramType: string,
paramFormat: string
) {
const hasMultiCollection = getHasMultiCollection(paramType, paramFormat);
const hasPipeCollection = getHasPipeCollection(paramType, paramFormat);
const hasSsvCollection = getHasSsvCollection(paramType, paramFormat);
const hasTsvCollection = getHasTsvCollection(paramType, paramFormat);
const hasCsvCollection = getHasCsvCollection(paramType, paramFormat);
const descriptions = [];
const collectionInfo = [];
if (hasMultiCollection) {
descriptions.push("buildMultiCollection");
collectionInfo.push("multi");
}
if (hasSsvCollection) {
descriptions.push("buildSsvCollection");
collectionInfo.push("ssv");
}
if (hasTsvCollection) {
descriptions.push("buildTsvCollection");
collectionInfo.push("tsv");
}
if (hasPipeCollection) {
descriptions.push("buildPipeCollection");
collectionInfo.push("pipe");
}
if (hasCsvCollection) {
descriptions.push("buildCsvCollection");
collectionInfo.push("csv");
}
return {
hasMultiCollection,
hasPipeCollection,
hasSsvCollection,
hasTsvCollection,
hasCsvCollection,
descriptions,
collectionInfo
};
}
function getHasMultiCollection(paramType: string, paramFormat: string) {
return (
(paramType === "query" || paramType === "header") && paramFormat === "multi"
);
}
function getHasSsvCollection(paramType: string, paramFormat: string) {
return paramType === "query" && paramFormat === "ssv";
}
function getHasTsvCollection(paramType: string, paramFormat: string) {
return paramType === "query" && paramFormat === "tsv";
}
function getHasCsvCollection(paramType: string, paramFormat: string) {
return paramType === "header" && paramFormat === "csv";
}
function getHasPipeCollection(paramType: string, paramFormat: string) {
return paramType === "query" && paramFormat === "pipes";
}
export function hasCollectionFormatInfo(
paramType: string,
paramFormat: string
) {
return (
getHasMultiCollection(paramType, paramFormat) ||
getHasSsvCollection(paramType, paramFormat) ||
getHasTsvCollection(paramType, paramFormat) ||
getHasCsvCollection(paramType, paramFormat) ||
getHasPipeCollection(paramType, paramFormat)
);
}
export function getCollectionFormatHelper(
paramType: string,
paramFormat: string
) {
const detail = getSpecialSerializeInfo(paramType, paramFormat);
return detail.descriptions.length > 0 ? detail.descriptions[0] : undefined;
}
export function getCustomRequestHeaderNameForOperation(
route: HttpOperation
): string | undefined {
const params = route.parameters.parameters.filter(
isCustomClientRequestIdParam
);
if (params.length > 0) {
return "client-request-id";
}
return undefined;
}
export function isCustomClientRequestIdParam(param: HttpOperationParameter) {
return (
param.type === "header" && param.name.toLowerCase() === "client-request-id"
);
}
export function isIgnoredHeaderParam(param: HttpOperationParameter) {
return (
isCustomClientRequestIdParam(param) ||
(param.type === "header" &&
["return-client-request-id", "ocp-date"].includes(
param.name.toLowerCase()
))
);
}
export function parseNextLinkName(
paged: PagedResultMetadata
): string | undefined {
return paged.nextLinkProperty?.name;
}
export function parseItemName(paged: PagedResultMetadata): string | undefined {
// TODO: support the nested item names
return (paged.itemsSegments ?? [])[0];
}

Просмотреть файл

@ -0,0 +1,92 @@
import {
SdkClient,
SdkContext,
listOperationGroups,
listOperationsInOperationGroup
} from "@azure-tools/typespec-client-generator-core";
import { ignoreDiagnostics, Model, Program, Type } from "@typespec/compiler";
import { getHttpOperation, HttpOperation } from "@typespec/http";
import {
hasPagingOperations,
extractPagedMetadataNested,
parseNextLinkName,
parseItemName
} from "../utils/operationUtil.js";
const pageableOperationsKey = Symbol("pageable");
export function getPageable(
program: Program,
entity: Type
): string | undefined {
return program.stateMap(pageableOperationsKey).get(entity);
}
export function extractPageDetailFromCore(
program: Program,
client: SdkClient,
dpgContext: SdkContext
) {
if (!hasPagingOperations(program, client, dpgContext)) {
return;
}
const nextLinks = new Set<string>();
const itemNames = new Set<string>();
// Add default values
nextLinks.add("nextLink");
itemNames.add("value");
const clientOperations = listOperationsInOperationGroup(dpgContext, client);
for (const clientOp of clientOperations) {
const route = ignoreDiagnostics(getHttpOperation(program, clientOp));
// ignore overload base operation
if (route.overloads && route.overloads?.length > 0) {
continue;
}
extractPageDetailFromCoreForRoute(route);
}
const operationGroups = listOperationGroups(dpgContext, client, true);
for (const operationGroup of operationGroups) {
const operations = listOperationsInOperationGroup(
dpgContext,
operationGroup
);
for (const op of operations) {
const route = ignoreDiagnostics(getHttpOperation(program, op));
// ignore overload base operation
if (route.overloads && route.overloads?.length > 0) {
continue;
}
extractPageDetailFromCoreForRoute(route);
}
}
function extractPageDetailFromCoreForRoute(route: HttpOperation) {
for (const response of route.responses) {
const paged = extractPagedMetadataNested(program, response.type as Model);
if (paged) {
const nextLinkName = parseNextLinkName(paged);
if (nextLinkName) {
nextLinks.add(nextLinkName);
}
const itemName = parseItemName(paged);
if (itemName) {
itemNames.add(itemName);
}
// Once we find paged metadata, we don't need to processs any further.
continue;
}
}
}
// If there are more than one options for nextLink and item names we need to generate a
// more complex pagination helper.
const isComplexPaging = nextLinks.size > 1 || itemNames.size > 1;
return {
hasPaging: true,
pageDetails: {
itemNames: [...itemNames],
nextLinkNames: [...nextLinks],
isComplexPaging
}
};
}

Просмотреть файл

@ -0,0 +1,10 @@
import { strictEqual } from "node:assert";
import { describe, it } from "vitest";
import { emit } from "./test-host.js";
describe("hello", () => {
it("emit output.txt with content hello world", async () => {
const results = await emit(`op test(): void;`);
strictEqual(results["output.txt"], "Hello world\n");
});
});

Просмотреть файл

@ -0,0 +1,47 @@
import { Diagnostic, resolvePath } from "@typespec/compiler";
import {
createTestHost,
createTestWrapper,
expectDiagnosticEmpty,
} from "@typespec/compiler/testing";
import { PowershellTsTestLibrary } from "../src/testing/index.js";
export async function createPowershellTsTestHost() {
return createTestHost({
libraries: [PowershellTsTestLibrary],
});
}
export async function createPowershellTsTestRunner() {
const host = await createPowershellTsTestHost();
return createTestWrapper(host, {
compilerOptions: {
noEmit: false,
emit: ["powershell-ts"],
},
});
}
export async function emitWithDiagnostics(
code: string
): Promise<[Record<string, string>, readonly Diagnostic[]]> {
const runner = await createPowershellTsTestRunner();
await runner.compileAndDiagnose(code, {
outputDir: "tsp-output",
});
const emitterOutputDir = "./tsp-output/powershell-ts";
const files = await runner.program.host.readDir(emitterOutputDir);
const result: Record<string, string> = {};
for (const file of files) {
result[file] = (await runner.program.host.readFile(resolvePath(emitterOutputDir, file))).text;
}
return [result, runner.program.diagnostics];
}
export async function emit(code: string): Promise<Record<string, string>> {
const [result, diagnostics] = await emitWithDiagnostics(code);
expectDiagnosticEmpty(diagnostics);
return result;
}

Просмотреть файл

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": false,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"rootDir": ".",
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"skipLibCheck": true,
/* Linting */
"strict": true
},
"include": ["src", "test"]
}