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

[tcgc] support generic type decorators in sdk types #966

Merged
merged 27 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
03efd05
initial version
tadelesh Jun 6, 2024
411a17e
changelog
tadelesh Jun 6, 2024
9546ed4
review
tadelesh Jun 7, 2024
a181a88
Merge branch 'main' into decorators_list
tadelesh Jun 7, 2024
ac0976b
Merge remote-tracking branch 'origin/main' into decorators_list
tadelesh Jun 12, 2024
7ff40cc
refine with review
tadelesh Jun 12, 2024
e844262
Merge branch 'main' into decorators_list
tadelesh Jun 12, 2024
60736e5
Merge branch 'main' into decorators_list
tadelesh Jun 13, 2024
f072219
Merge branch 'main' into decorators_list
tadelesh Jun 14, 2024
bd57509
refine with review
tadelesh Jun 14, 2024
367e95e
Merge remote-tracking branch 'origin/main' into decorators_list
tadelesh Jun 17, 2024
a65efd8
format
tadelesh Jun 17, 2024
7c9249e
update with review
tadelesh Jun 18, 2024
017c3b2
Merge remote-tracking branch 'origin/main' into decorators_list
tadelesh Jun 18, 2024
30a9052
Merge remote-tracking branch 'origin/main' into decorators_list
tadelesh Jun 18, 2024
16635c8
update dep
tadelesh Jun 18, 2024
12223e7
Merge branch 'main' into decorators_list
tadelesh Jun 19, 2024
4ab74a6
refine with review
tadelesh Jun 19, 2024
d63952a
fix
tadelesh Jun 19, 2024
079ffde
fix diagnostic issue and add test
tadelesh Jun 19, 2024
157a726
format, lint, add `@useFinalStateVia` to white list and add test
tadelesh Jun 20, 2024
c099e4e
rename and doc
tadelesh Jun 24, 2024
93be6bb
refine
tadelesh Jun 25, 2024
a60f7f2
Merge remote-tracking branch 'origin/main' into decorators_list
tadelesh Jun 25, 2024
7426d3c
typo and format
tadelesh Jun 25, 2024
a540867
Merge remote-tracking branch 'origin/main' into decorators_list
tadelesh Jun 26, 2024
0f63c09
update dep
tadelesh Jun 26, 2024
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
7 changes: 7 additions & 0 deletions .chronus/changes/decorators_list-2024-5-6-18-38-7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

export decorators in white list to all sdk types
tadelesh marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 3 additions & 1 deletion packages/typespec-client-generator-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@
"@typespec/compiler": "workspace:~",
"@typespec/http": "workspace:~",
"@typespec/rest": "workspace:~",
"@typespec/versioning": "workspace:~"
"@typespec/versioning": "workspace:~",
"@typespec/xml": "workspace:~"
},
"devDependencies": {
"@azure-tools/typespec-azure-core": "workspace:~",
"@types/node": "~18.11.19",
"@types/pluralize": "^0.0.33",
"@typespec/compiler": "workspace:~",
"@typespec/http": "workspace:~",
"@typespec/xml": "workspace:~",
"@typespec/library-linter": "workspace:~",
"@typespec/prettier-plugin-typespec": "workspace:~",
"@typespec/rest": "workspace:~",
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-client-generator-core/src/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const defaultDecoratorsWhiteList = ["TypeSpec\\.Xml\\..*"];
9 changes: 7 additions & 2 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from "@typespec/compiler";
import { isHeader } from "@typespec/http";
import { buildVersionProjections, getVersions } from "@typespec/versioning";
import { defaultDecoratorsWhiteList } from "./configs.js";
import {
AccessFlags,
LanguageScopes,
Expand Down Expand Up @@ -313,6 +314,7 @@ export function listClients(context: TCGCContext): SdkClient[] {
}

const operationGroupKey = createStateSymbol("operationGroup");

export function $operationGroup(
context: DecoratorContext,
target: Namespace | Interface,
Expand Down Expand Up @@ -412,6 +414,7 @@ function buildOperationGroupPath(context: TCGCContext, type: Namespace | Interfa
}
return path.reverse().join(".");
}

/**
* Return the operation group object for the given namespace or interface or undefined is not an operation group.
* @param context TCGCContext
Expand Down Expand Up @@ -570,9 +573,9 @@ export function listOperationsInOperationGroup(
addOperations(group.type);
return operations;
}

interface CreateSdkContextOptions {
export interface CreateSdkContextOptions {
readonly versionStrategy?: "ignore";
additionalDecorators?: string[];
}

export function createSdkContext<
Expand Down Expand Up @@ -607,6 +610,7 @@ export function createSdkContext<
__namespaceToApiVersionParameter: new Map(),
__tspTypeToApiVersions: new Map(),
__namespaceToApiVersionClientDefaultValue: new Map(),
decoratorsWhiteList: [...defaultDecoratorsWhiteList, ...(options?.additionalDecorators ?? [])],
};
sdkContext.experimental_sdkPackage = getSdkPackage(sdkContext);
if (sdkContext.diagnostics) {
Expand Down Expand Up @@ -706,6 +710,7 @@ function modelTransitiveSet(
}

const clientFormatKey = createStateSymbol("clientFormat");

const allowedClientFormatToTargetTypeMap: Record<ClientFormat, string[]> = {
unixtime: ["int32", "int64"],
iso8601: ["utcDateTime", "offsetDateTime", "duration"],
Expand Down
5 changes: 5 additions & 0 deletions packages/typespec-client-generator-core/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
getAvailableApiVersions,
getDocHelper,
getLocationOfOperation,
getTypeDecorators,
isAcceptHeader,
isContentTypeHeader,
isNeverOrVoidType,
Expand Down Expand Up @@ -166,6 +167,7 @@ function getSdkHttpParameters(
optional: false,
correspondingMethodParams,
crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, httpOperation.operation)}.body`,
decorators: diagnostics.pipe(getTypeDecorators(context, tspBody.type)),
};
}
if (retval.bodyParam) {
Expand Down Expand Up @@ -234,6 +236,7 @@ function createContentTypeOrAcceptHeader(
let type: SdkType = {
kind: "string",
encode: "string",
decorators: {},
};
// for contentType, we treat it as a constant IFF there's one value and it's application/json.
// this is to prevent a breaking change when a service adds more content types in the future.
Expand All @@ -254,6 +257,7 @@ function createContentTypeOrAcceptHeader(
valueType: type,
name: `${httpOperation.operation.name}ContentType`,
isGeneratedName: true,
decorators: {},
};
}
// No need for clientDefaultValue because it's a constant, it only has one value
Expand All @@ -267,6 +271,7 @@ function createContentTypeOrAcceptHeader(
onClient: false,
optional: false,
crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, httpOperation.operation)}.${name}`,
decorators: {},
};
}

Expand Down
13 changes: 9 additions & 4 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export interface SdkInitializationType extends SdkModelType {
properties: SdkParameter[];
}

export interface SdkClientType<TServiceOperation extends SdkServiceOperation> {
export interface SdkClientType<TServiceOperation extends SdkServiceOperation>
extends DecoratedType {
kind: "client";
name: string;
description?: string;
Expand All @@ -77,7 +78,11 @@ export interface SdkOperationGroup {
service: Namespace;
}

interface SdkTypeBase {
interface DecoratedType {
tadelesh marked this conversation as resolved.
Show resolved Hide resolved
decorators: Record<string, Record<string, any>>;
jhendrixMSFT marked this conversation as resolved.
Show resolved Hide resolved
}

interface SdkTypeBase extends DecoratedType {
tadelesh marked this conversation as resolved.
Show resolved Hide resolved
__raw?: Type;
kind: string;
deprecation?: string;
Expand Down Expand Up @@ -326,7 +331,7 @@ export interface SdkEndpointType extends SdkTypeBase {
templateArguments: SdkPathParameter[];
}

export interface SdkModelPropertyTypeBase {
export interface SdkModelPropertyTypeBase extends DecoratedType {
__raw?: ModelProperty;
type: SdkType;
/**
Expand Down Expand Up @@ -469,7 +474,7 @@ export interface SdkHttpOperation extends SdkServiceOperationBase {
export type SdkServiceOperation = SdkHttpOperation;
export type SdkServiceParameter = SdkHttpParameter;

interface SdkMethodBase {
interface SdkMethodBase extends DecoratedType {
__raw?: Operation;
name: string;
access: AccessFlags;
Expand Down
92 changes: 86 additions & 6 deletions packages/typespec-client-generator-core/src/internal-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
Interface,
Model,
Namespace,
Numeric,
NumericLiteral,
Operation,
Program,
ProjectedProgram,
StringLiteral,
Type,
Union,
Value,
createDiagnosticCollector,
getDeprecationDetails,
getDoc,
Expand Down Expand Up @@ -242,6 +244,7 @@ interface DefaultSdkTypeBase<TKind> {
__raw: Type;
deprecation?: string;
kind: TKind;
decorators: Record<string, Record<string, any>>;
}

/**
Expand All @@ -252,12 +255,83 @@ export function getSdkTypeBaseHelper<TKind>(
context: TCGCContext,
type: Type,
kind: TKind
): DefaultSdkTypeBase<TKind> {
return {
): [DefaultSdkTypeBase<TKind>, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
return diagnostics.wrap({
__raw: type,
deprecation: getDeprecationDetails(context.program, type)?.message,
kind,
};
decorators: diagnostics.pipe(getTypeDecorators(context, type)),
});
}

export function getNamespacePrefix(namespace: Namespace): string {
return namespace ? getNamespaceFullName(namespace) + "." : "";
}

export function getTypeDecorators(
context: TCGCContext,
type: Type
): [Record<string, Record<string, any>>, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const retval: Record<string, Record<string, any>> = {};
if ("decorators" in type) {
for (const decorator of type.decorators) {
// only process explicitly defined decorators
if (decorator.definition) {
const decoratorName = `${getNamespacePrefix(decorator.definition?.namespace)}${decorator.definition?.name}`;
// white list filtering
if (
!context.decoratorsWhiteList ||
!context.decoratorsWhiteList.some((x) => new RegExp(x).test(decoratorName))
) {
continue;
}

retval[decoratorName] = {};
for (let i = 0; i < decorator.args.length; i++) {
retval[decoratorName][decorator.definition.parameters[i].name] = diagnostics.pipe(
getDecoratorArgValue(decorator.args[i].jsValue, type, decoratorName)
);
}
}
}
}
return diagnostics.wrap(retval);
}

function getDecoratorArgValue(
arg:
| Type
| Record<string, unknown>
| Value
| unknown[]
| string
| number
| boolean
| Numeric
| null,
type: Type,
decoratorName: string
): [any, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
if (typeof arg === "object" && arg !== null && "kind" in arg) {
tadelesh marked this conversation as resolved.
Show resolved Hide resolved
if (arg.kind === "EnumMember") {
return diagnostics.wrap(arg.value ?? arg.name);
}
if (arg.kind === "String" || arg.kind === "Number" || arg.kind === "Boolean") {
return diagnostics.wrap(arg.value);
}
diagnostics.add(
createDiagnostic({
code: "unsupported-generic-decorator-arg-type",
target: type,
format: { decoratorName },
})
);
return diagnostics.wrap(undefined);
}
return diagnostics.wrap(arg);
}

export function intOrFloat(value: number): "int32" | "float32" {
Expand Down Expand Up @@ -325,6 +399,7 @@ export interface TCGCContext {
apiVersion?: string;
__service_projection?: Map<Namespace, [Namespace, ProjectedProgram | undefined]>;
originalProgram: Program;
decoratorsWhiteList?: string[];
}

export function createTCGCContext(program: Program): TCGCContext {
Expand Down Expand Up @@ -426,9 +501,14 @@ export function isNeverOrVoidType(type: Type): boolean {
return isNeverType(type) || isVoidType(type);
}

export function getAnyType(): SdkBuiltInType {
return {
export function getAnyType(
context: TCGCContext,
type: Type
): [SdkBuiltInType, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
return diagnostics.wrap({
kind: "any",
encode: "string",
};
decorators: diagnostics.pipe(getTypeDecorators(context, type)),
});
}
12 changes: 9 additions & 3 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,25 @@ export const $lib = createTypeSpecLibrary({
"no-corresponding-method-param": {
severity: "error",
messages: {
default: `Missing "${"paramName"}" method parameter in method "${"methodName"}", when "${"paramName"}" must be sent to the service. Add a parameter named "${"paramName"}" to the method.`,
default: paramMessage`Missing "${"paramName"}" method parameter in method "${"methodName"}", when "${"paramName"}" must be sent to the service. Add a parameter named "${"paramName"}" to the method.`,
},
},
"unsupported-protocol": {
severity: "error",
messages: {
default: paramMessage`Currently we only support HTTP and HTTPS protocols`,
default: "Currently we only support HTTP and HTTPS protocols",
},
},
"no-emitter-name": {
severity: "warning",
messages: {
default: paramMessage`Can not find name for your emitter, please check your emitter name.`,
default: "Can not find name for your emitter, please check your emitter name.",
},
},
"unsupported-generic-decorator-arg-type": {
severity: "warning",
messages: {
default: paramMessage`Can not parse the arg type for decorator "${"decoratorName"}".`,
},
},
},
Expand Down
Loading
Loading