Skip to content

Commit

Permalink
Inlcude csharp rename decorator when openai-to-typespec generating tsp (
Browse files Browse the repository at this point in the history
#4907)

Fixes Azure/autorest.csharp#4236.
Generate csharp renaming decorator when openai-to-typespec generating
tsp. (include rename of resource, resource.property, model,
model.property, enum, enum.member and operation name)

related autorest.csharp change can be found at
Azure/autorest.csharp#4380
  • Loading branch information
RodgeFu authored Mar 19, 2024
1 parent cc5a4dc commit 63ffe68
Show file tree
Hide file tree
Showing 16 changed files with 357 additions and 35 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,4 @@ regression-tests/output

# TS incremental build cache
*.tsbuildinfo
*.njsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@autorest/openapi-to-typespec",
"comment": "support generating csharp rename decorator when converting to tsp",
"type": "minor"
}
],
"packageName": "@autorest/openapi-to-typespec"
}
16 changes: 7 additions & 9 deletions packages/extensions/openapi-to-typespec/convert.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,18 @@
#>

param(
[Parameter(Mandatory)]
[string]
# Specifies the swagger config file, not the swagger json, but the readme config.
[Parameter(Mandatory = $true, HelpMessage = "Specifies the swagger config file (not the swagger json, but the readme config) or autorest.md file in the azure-sdk-for-net repo if .net related configuration is expected to be included.")]
[string]
$swaggerConfigFile,
[Parameter(Mandatory = $false, HelpMessage = "Specified the output folder, deafult to current folder.")]
[string]
# Specified the output folder, deafult to current folder.
$outputFolder,
[Parameter(Mandatory = $false, HelpMessage = "Specified the csharp codegen, default to https://aka.ms/azsdk/openapi-to-typespec-csharp.")]
[string]
# Specified the csharp codegen, default to https://aka.ms/azsdk/openapi-to-typespec-csharp.
$csharpCodegen = "https://aka.ms/azsdk/openapi-to-typespec-csharp",
[Parameter(Mandatory = $false, HelpMessage = "Specified the converter codegen, default to https://aka.ms/azsdk/openapi-to-typespec.")]
[string]
# Specified the converter codegen, default to https://aka.ms/azsdk/openapi-to-typespec.
$converterCodegen = "."
)
$converterCodegen = ".")

function GenerateMetadata ()
{
Expand All @@ -42,7 +40,7 @@ function GenerateMetadata ()
function DoConvert ()
{
Write-Host "##Converting from swagger to tsp with in $outputFolder with $converterCodegen"
$cmd = "autorest --version=3.10.1 --openapi-to-typespec --isAzureSpec --isArm --use=`"$converterCodegen`" --output-folder=$outputFolder $swaggerConfigFile"
$cmd = "autorest --version=3.10.1 --openapi-to-typespec --csharp=false --isAzureSpec --isArm --use=`"$converterCodegen`" --output-folder=$outputFolder $swaggerConfigFile"
Write-Host "$cmd"
Invoke-Expression $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
Expand Down
16 changes: 13 additions & 3 deletions packages/extensions/openapi-to-typespec/src/emiters/emit-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { getSession } from "../autorest-session";
import { generateArmResourceClientDecorator, generateObjectClientDecorator } from "../generate/generate-client";
import {
generateArmResourceClientDecorator,
generateEnumClientDecorator,
generateObjectClientDecorator,
} from "../generate/generate-client";
import { TypespecProgram } from "../interfaces";
import { getOptions } from "../options";
import { formatTypespecFile } from "../utils/format";
Expand Down Expand Up @@ -34,8 +38,14 @@ function generateClient(program: TypespecProgram) {
.filter((r) => r !== "")
.join("\n\n")
: "";
if (objects === "" && armResources === "") {

const enums = models.enums
.map(generateEnumClientDecorator)
.filter((r) => r !== "")
.join("\n\n");

if (objects === "" && armResources === "" && enums === "") {
return "";
}
return [imports, "\n", namespaces, "\n", objects, "\n", armResources].join("\n");
return [imports, "\n", namespaces, "\n", objects, "\n", armResources, "\n", enums].join("\n");
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ function getArmResourceImports(program: TypespecProgram): string[] {
const resourceMetadata = getArmResourcesMetadata();
const imports: string[] = [];

for (const resource in resourceMetadata) {
imports.push(`import "./${resourceMetadata[resource].SwaggerModelName}.tsp";`);
for (const resource in resourceMetadata.Resources) {
imports.push(`import "./${resourceMetadata.Resources[resource].SwaggerModelName}.tsp";`);
}

if (program.operationGroups.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import pluralize from "pluralize";
import { TspArmResource, TypespecObject } from "../interfaces";
import { TspArmResource, TypespecObject, TypespecEnum, TypespecOperation } from "../interfaces";
import { generateAugmentedDecorators } from "../utils/decorators";

export function generateObjectClientDecorator(typespecObject: TypespecObject) {
const definitions: string[] = [];

definitions.push(generateAugmentedDecorators(typespecObject.name, typespecObject.clientDecorators));

for (const property of typespecObject.properties) {
const decorators = generateAugmentedDecorators(
`${typespecObject.name}.${property.name}`,
Expand All @@ -16,11 +18,50 @@ export function generateObjectClientDecorator(typespecObject: TypespecObject) {
return definitions.join("\n");
}

export function generateEnumClientDecorator(typespecEnum: TypespecEnum) {
const definitions: string[] = [];

definitions.push(generateAugmentedDecorators(typespecEnum.name, typespecEnum.clientDecorators));

for (const choice of typespecEnum.members) {
const decorators = generateAugmentedDecorators(`${typespecEnum.name}.${choice.name}`, choice.clientDecorators);
decorators && definitions.push(decorators);
}

return definitions.join("\n");
}

export function generateOperationClientDecorator(operation: TypespecOperation) {
const definitions: string[] = [];

definitions.push(generateAugmentedDecorators(operation.name, operation.clientDecorators));

return definitions.join("\n");
}

export function generateArmResourceClientDecorator(resource: TspArmResource): string {
const definitions: string[] = [];

const formalOperationGroupName = pluralize(resource.name);
let targetName = formalOperationGroupName;

if (resource.name === formalOperationGroupName) {
return `@@clientName(${formalOperationGroupName}OperationGroup, "${formalOperationGroupName}")`;
targetName = `${formalOperationGroupName}OperationGroup}`;
definitions.push(`@@clientName(${formalOperationGroupName}OperationGroup, "${formalOperationGroupName}")`);
}
return "";

if (resource.clientDecorators && resource.clientDecorators.length > 0)
definitions.push(generateAugmentedDecorators(resource.name, resource.clientDecorators));

for (const op of resource.resourceOperations) {
if (op.clientDecorators && op.clientDecorators.length > 0)
definitions.push(generateAugmentedDecorators(`${targetName}.${op.name}`, op.clientDecorators));
}

for (const property of resource.properties) {
const decorators = generateAugmentedDecorators(`${targetName}.${property.name}`, property.clientDecorators);
decorators && definitions.push(decorators);
}

return definitions.join("\n");
}
6 changes: 6 additions & 0 deletions packages/extensions/openapi-to-typespec/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface TypespecOptions {
export interface TypespecChoiceValue extends WithDoc {
name: string;
value: string | number | boolean;
clientDecorators?: TypespecDecorator[];
}

export interface WithDoc {
Expand Down Expand Up @@ -42,6 +43,7 @@ export interface TypespecOperation extends WithDoc, WithSummary, WithFixMe {
operationGroupName?: string;
operationId?: string;
examples?: Record<string, Record<string, unknown>>;
clientDecorators?: TypespecDecorator[];
}

export type ResourceKind =
Expand Down Expand Up @@ -120,6 +122,7 @@ export interface TypespecEnum extends TypespecDataType {
members: TypespecChoiceValue[];
isExtensible: boolean;
decorators?: TypespecDecorator[];
clientDecorators?: TypespecDecorator[];
}

export interface WithFixMe {
Expand All @@ -137,6 +140,7 @@ export interface TypespecParameter extends TypespecDataType {
isOptional: boolean;
type: string;
decorators?: TypespecDecorator[];
clientDecorators?: TypespecDecorator[];
location: TypespecParameterLocation;
serializedName: string;
defaultValue?: any;
Expand Down Expand Up @@ -171,6 +175,7 @@ export interface TypespecObject extends TypespecDataType {
extendedParents?: string[];
spreadParents?: string[];
decorators?: TypespecDecorator[];
clientDecorators?: TypespecDecorator[];
alias?: TypespecAlias;
}

Expand Down Expand Up @@ -201,6 +206,7 @@ export interface TspArmResourceOperationBase extends WithDoc, WithFixMe {
name: string;
templateParameters?: string[];
decorators?: TypespecDecorator[];
clientDecorators?: TypespecDecorator[];
operationId?: string;
examples?: Record<string, Record<string, unknown>>;
customizations?: string[];
Expand Down
2 changes: 2 additions & 0 deletions packages/extensions/openapi-to-typespec/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { emitTypespecConfig } from "./emiters/emit-typespec-config";
import { getModel } from "./model";
import { pretransformArmResources } from "./pretransforms/arm-pretransform";
import { pretransformNames } from "./pretransforms/name-pretransform";
import { pretransformRename } from "./pretransforms/rename-pretransform";
import { markErrorModels } from "./utils/errors";
import { markPagination } from "./utils/paging";
import { markResources } from "./utils/resources";
Expand All @@ -27,6 +28,7 @@ export async function processConverter(host: AutorestExtensionHost) {
const codeModel = session.model;
pretransformNames(codeModel);
pretransformArmResources(codeModel);
pretransformRename(codeModel);
markPagination(codeModel);
markErrorModels(codeModel);
markResources(codeModel);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
ChoiceSchema,
CodeModel,
ObjectSchema,
SealedChoiceSchema,
Schema,
ChoiceValue,
Property,
Parameter,
SchemaType,
Operation,
} from "@autorest/codemodel";
import { TypespecDecorator } from "../interfaces";
import { getOptions } from "../options";
import { getLogger } from "../utils/logger";
import { Metadata, getArmResourcesMetadata } from "../utils/resource-discovery";

type RenamableSchema = Schema | Property | Parameter | ChoiceValue | Operation;

const logger = () => getLogger("rename-pretransform");

export function pretransformRename(codeModel: CodeModel): void {
const { isArm } = getOptions();
if (!isArm) {
return;
}

const metadata = getArmResourcesMetadata();

applyRenameMapping(metadata, codeModel);
applyOverrideOperationName(metadata, codeModel);
}

export function createCSharpNameDecorator(schema: RenamableSchema): TypespecDecorator {
return {
name: "clientName",
module: "@azure-tools/typespec-client-generator-core",
namespace: "Azure.ClientGenerator.Core",
arguments: [schema.language.csharp!.name, "csharp"],
};
}

function parseNewCSharpNameAndSetToSchema(schema: RenamableSchema, renameValue: string) {
const newName = parseNewName(renameValue);
setSchemaCSharpName(schema, newName);
}

function setSchemaCSharpName(schema: RenamableSchema, newName: string) {
if (!schema.language.csharp)
schema.language.csharp = { name: newName, description: schema.language.default.description };
else schema.language.csharp.name = newName;
}

function parseNewName(value: string) {
// TODO: format not supported
return value.split("|")[0].trim();
}

function applyOverrideOperationName(metadata: Metadata, codeModel: CodeModel) {
for (const opId in metadata.OverrideOperationName) {
const found = codeModel.operationGroups.flatMap((og) => og.operations).find((op) => op.operationId === opId);
if (found) parseNewCSharpNameAndSetToSchema(found, metadata.OverrideOperationName[opId]);
else
logger().warning(
`Can't find operation to rename for OverrideOperationName rule: ${opId}->${metadata.OverrideOperationName[opId]}`,
);
}
}

function applyRenameMapping(metadata: Metadata, codeModel: CodeModel) {
for (const key in metadata.RenameMapping) {
const subKeys = key
.split(".")
.map((s) => s.trim())
.filter((s) => s.length > 0);
if (subKeys.length === 0) continue;
const lowerFirstSubKey = subKeys[0].toLowerCase();
const value = metadata.RenameMapping[key];

const found: Schema | undefined = [
...(codeModel.schemas.choices ?? []),
...(codeModel.schemas.sealedChoices ?? []),
...(codeModel.schemas.objects ?? []),
].find((o: Schema) => o.language.default.name.toLowerCase() === lowerFirstSubKey);

if (!found) {
logger().warning(`Can't find object or enum for RenameMapping rule: ${key} -> ${value}`);
continue;
}

if (found.type === SchemaType.Choice || found.type == SchemaType.SealedChoice) {
transformEnum(subKeys, value, found as ChoiceSchema | SealedChoiceSchema);
} else if (found.type === SchemaType.Object) {
transformObject(subKeys, value, found as ObjectSchema);
} else {
logger().error(`Unexpected schema type '${found.type}' found with key ${key}`);
}
}
}

function transformEnum(keys: string[], value: string, target: ChoiceSchema | SealedChoiceSchema) {
if (keys.length === 1) parseNewCSharpNameAndSetToSchema(target, value);
else if (keys.length === 2) {
const lowerMemberValue = keys[1].toLowerCase();
const found = target.choices.find((c) => c.language.default.name.toLowerCase() === lowerMemberValue);
if (found) parseNewCSharpNameAndSetToSchema(found, value);
else logger().warning(`Can't find enum member for RenameMapping rule: ${keys.join(".")} -> ${value}`);
} else {
logger().error(`Unexpected keys for enum RenameMapping: ${keys.join(".")}`);
}
}

function transformObject(keys: string[], value: string, target: ObjectSchema) {
if (keys.length === 1) parseNewCSharpNameAndSetToSchema(target, value);
else if (keys.length === 2) {
const lowerPropertyName = keys[1].toLowerCase();
const found = target.properties?.find((p) => p.language.default.name.toLowerCase() === lowerPropertyName);
if (found) parseNewCSharpNameAndSetToSchema(found, value);
else logger().warning(`Can't find object property for RenameMapping rule: ${keys.join(".")} -> ${value}`);
} else if (keys.length > 2) {
// handle flatten scenario
const lowerPropName = keys.pop()?.toLowerCase();
let cur = target;
for (let i = 1; i < keys.length && cur; i++) {
const foundProp = cur.properties?.find((p) => p.language.default.name.toLowerCase() === keys[i].toLowerCase());
cur = foundProp?.schema as ObjectSchema;
}
const foundProp = cur?.properties?.find((p) => p.language.default.name.toLowerCase() === lowerPropName);
if (foundProp) parseNewCSharpNameAndSetToSchema(foundProp, value);
else {
logger().warning(`Can't find object property for RenameMapping rule: ${keys.join(".")} -> ${value}`);
}
} else {
logger().error(`Unexpected keys for object property RenameMapping: ${keys.join(".")}`);
}
}
Loading

0 comments on commit 63ffe68

Please sign in to comment.