Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inlcude csharp rename decorator when openai-to-typespec generating tsp #4907

Merged
merged 18 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
6 changes: 5 additions & 1 deletion packages/extensions/openapi-to-typespec/convert.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ param(
[string]
# Specifies the swagger config file, not the swagger json, but the readme config.
$swaggerConfigFile,
[Parameter(Mandatory)]
[string]
# Specifies the corresponding autorest.md file in azure-sdk-for-net repo used by autorest.csharp codegen.
$csharpAutorestFile,
RodgeFu marked this conversation as resolved.
Show resolved Hide resolved
[string]
# Specified the output folder, deafult to current folder.
$outputFolder,
Expand All @@ -28,7 +32,7 @@ param(
function GenerateMetadata ()
{
Write-Host "##Generating metadata with csharp codegen in $outputFolder with $csharpCodegen"
$cmd = "autorest --version=3.10.1 --csharp --isAzureSpec --isArm --max-memory-size=8192 --use=`"$csharpCodegen`" --output-folder=$outputFolder --mgmt-debug.only-generate-metadata --azure-arm --skip-csproj $swaggerConfigFile"
$cmd = "autorest --version=3.10.1 --csharp --isAzureSpec --isArm --max-memory-size=8192 --use=`"$csharpCodegen`" --output-folder=$outputFolder --mgmt-debug.only-generate-metadata --azure-arm --skip-csproj $csharpAutorestFile"
Write-Host "$cmd"
Invoke-Expression $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
Expand Down
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
Loading