Skip to content

Commit

Permalink
[tcgc] add $onValidate (#315)
Browse files Browse the repository at this point in the history
  • Loading branch information
iscai-msft authored Feb 26, 2024
1 parent 987fe50 commit dac6668
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 102 deletions.
7 changes: 7 additions & 0 deletions .chronus/changes/add_on_validate-2024-1-26-14-11-57.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

add validation on import of tcgc and remove duplicate validation warnings
78 changes: 42 additions & 36 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,21 @@ import {
SdkEmitterOptions,
SdkOperationGroup,
} from "./interfaces.js";
import { parseEmitterName } from "./internal-utils.js";
import { TCGCContext, createTCGCContext, parseEmitterName } from "./internal-utils.js";
import { createStateSymbol, reportDiagnostic } from "./lib.js";
import { getAllModels, getSdkEnum, getSdkModel } from "./types.js";

export const namespace = "Azure.ClientGenerator.Core";
const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/all-scopes");

function getScopedDecoratorData(context: SdkContext, key: symbol, target: Type): any {
function getScopedDecoratorData(context: TCGCContext, key: symbol, target: Type): any {
const retval: Record<string | symbol, any> = context.program.stateMap(key).get(target);
if (retval === undefined) return retval;
if (Object.keys(retval).includes(context.emitterName)) return retval[context.emitterName];
return retval[AllScopes]; // in this case it applies to all languages
}

function listScopedDecoratorData(context: SdkContext, key: symbol): any[] {
function listScopedDecoratorData(context: TCGCContext, key: symbol): any[] {
const retval = [...context.program.stateMap(key).values()];
return retval
.filter((targetEntry) => {
Expand Down Expand Up @@ -161,11 +161,14 @@ function findClientService(
/**
* Return the client object for the given namespace or interface, or undefined if the given namespace or interface is not a client.
*
* @param context SdkContext
* @param context TCGCContext
* @param type Type to check
* @returns Client or undefined
*/
export function getClient(context: SdkContext, type: Namespace | Interface): SdkClient | undefined {
export function getClient(
context: TCGCContext,
type: Namespace | Interface
): SdkClient | undefined {
if (hasExplicitClientOrOperationGroup(context)) {
return getScopedDecoratorData(context, clientKey, type);
}
Expand All @@ -183,7 +186,7 @@ export function getClient(context: SdkContext, type: Namespace | Interface): Sdk
return undefined;
}

function hasExplicitClientOrOperationGroup(context: SdkContext): boolean {
function hasExplicitClientOrOperationGroup(context: TCGCContext): boolean {
return (
listScopedDecoratorData(context, clientKey).length > 0 ||
listScopedDecoratorData(context, operationGroupKey).length > 0
Expand All @@ -193,10 +196,10 @@ function hasExplicitClientOrOperationGroup(context: SdkContext): boolean {
/**
* List all the clients.
*
* @param context SdkContext
* @param context TCGCContext
* @returns Array of clients
*/
export function listClients(context: SdkContext): SdkClient[] {
export function listClients(context: TCGCContext): SdkClient[] {
const explicitClients = [...listScopedDecoratorData(context, clientKey)];
if (explicitClients.length > 0) {
return explicitClients;
Expand Down Expand Up @@ -255,11 +258,11 @@ export function $operationGroup(

/**
* Check a namespace or interface is an operation group.
* @param context SdkContext
* @param context TCGCContext
* @param type Type to check
* @returns boolean
*/
export function isOperationGroup(context: SdkContext, type: Namespace | Interface): boolean {
export function isOperationGroup(context: TCGCContext, type: Namespace | Interface): boolean {
if (hasExplicitClientOrOperationGroup(context)) {
return getScopedDecoratorData(context, operationGroupKey, type) !== undefined;
}
Expand All @@ -274,12 +277,12 @@ export function isOperationGroup(context: SdkContext, type: Namespace | Interfac
}
/**
* Check an operation is in an operation group.
* @param context SdkContext
* @param context TCGCContext
* @param type Type to check
* @returns boolean
*/
export function isInOperationGroup(
context: SdkContext,
context: TCGCContext,
type: Namespace | Interface | Operation
): boolean {
switch (type.kind) {
Expand All @@ -298,7 +301,7 @@ export function isInOperationGroup(
}
}

function buildOperationGroupPath(context: SdkContext, type: Namespace | Interface): string {
function buildOperationGroupPath(context: TCGCContext, type: Namespace | Interface): string {
const path = [];
while (true) {
const client = getClient(context, type);
Expand All @@ -319,12 +322,12 @@ function buildOperationGroupPath(context: SdkContext, type: Namespace | Interfac
}
/**
* Return the operation group object for the given namespace or interface or undefined is not an operation group.
* @param context SdkContext
* @param context TCGCContext
* @param type Type to check
* @returns Operation group or undefined.
*/
export function getOperationGroup(
context: SdkContext,
context: TCGCContext,
type: Namespace | Interface
): SdkOperationGroup | undefined {
let operationGroup: SdkOperationGroup | undefined;
Expand Down Expand Up @@ -381,13 +384,13 @@ export function getOperationGroup(
/**
* List all the operation groups inside a client or an operation group. If ignoreHierarchy is true, the result will include all nested operation groups.
*
* @param context SdkContext
* @param context TCGCContext
* @param group Client or operation group to list operation groups
* @param ignoreHierarchy Whether to get all nested operation groups
* @returns
*/
export function listOperationGroups(
context: SdkContext,
context: TCGCContext,
group: SdkClient | SdkOperationGroup,
ignoreHierarchy = false
): SdkOperationGroup[] {
Expand Down Expand Up @@ -421,13 +424,13 @@ export function listOperationGroups(

/**
* List operations inside a client or an operation group. If ignoreHierarchy is true, the result will include all nested operations.
* @param program SdkContext
* @param program TCGCContext
* @param group Client or operation group to list operations
* @param ignoreHierarchy Whether to get all nested operations
* @returns
*/
export function listOperationsInOperationGroup(
context: SdkContext,
context: TCGCContext,
group: SdkOperationGroup | SdkClient,
ignoreHierarchy = false
): Operation[] {
Expand Down Expand Up @@ -477,7 +480,7 @@ export function createSdkContext<TOptions extends Record<string, any> = SdkEmitt
const generateConvenienceMethods =
context.options["generate-convenience-methods"] ?? convenienceOptions;
return {
program: context.program,
...createTCGCContext(context.program),
emitContext: context,
emitterName: parseEmitterName(emitterName ?? context.program.emitters[0]?.metadata?.name), // eslint-disable-line deprecation/deprecation
generateProtocolMethods: generateProtocolMethods,
Expand Down Expand Up @@ -509,14 +512,14 @@ export function $convenientAPI(
setScopedDecoratorData(context, $convenientAPI, convenientAPIKey, entity, value, scope);
}

export function shouldGenerateProtocol(context: SdkContext, entity: Operation): boolean {
export function shouldGenerateProtocol(context: TCGCContext, entity: Operation): boolean {
const value = getScopedDecoratorData(context, protocolAPIKey, entity);
return value ?? context.generateProtocolMethods;
return value ?? !!context.generateProtocolMethods;
}

export function shouldGenerateConvenient(context: SdkContext, entity: Operation): boolean {
export function shouldGenerateConvenient(context: TCGCContext, entity: Operation): boolean {
const value = getScopedDecoratorData(context, convenientAPIKey, entity);
return value ?? context.generateConvenienceMethods;
return value ?? !!context.generateConvenienceMethods;
}

const excludeKey = createStateSymbol("exclude");
Expand All @@ -540,14 +543,14 @@ export function $include(context: DecoratorContext, entity: Model, scope?: Langu
/**
* @deprecated This function is unused and will be removed in a future release.
*/
export function isExclude(context: SdkContext, entity: Model): boolean {
export function isExclude(context: TCGCContext, entity: Model): boolean {
return getScopedDecoratorData(context, excludeKey, entity) ?? false;
}

/**
* @deprecated This function is unused and will be removed in a future release.
*/
export function isInclude(context: SdkContext, entity: Model): boolean {
export function isInclude(context: TCGCContext, entity: Model): boolean {
return getScopedDecoratorData(context, includeKey, entity) ?? false;
}

Expand Down Expand Up @@ -619,7 +622,7 @@ export function $clientFormat(
* @deprecated This function is unused and will be removed in a future release.
*/
export function getClientFormat(
context: SdkContext,
context: TCGCContext,
entity: ModelProperty
): ClientFormat | undefined {
let retval: ClientFormat | undefined = getScopedDecoratorData(context, clientFormatKey, entity);
Expand Down Expand Up @@ -654,12 +657,15 @@ export function $internal(context: DecoratorContext, target: Operation, scope?:
* Whether a model / operation is internal or not. If it's internal, emitters
* should not expose them to users
*
* @param context SdkContext
* @param context TCGCContext
* @param entity model / operation that we want to check is internal or not
* @returns whether the entity is internal
* @deprecated This function is unused and will be removed in a future release.
*/
export function isInternal(context: SdkContext, entity: Model | Operation | Enum | Union): boolean {
export function isInternal(
context: TCGCContext,
entity: Model | Operation | Enum | Union
): boolean {
const found = getScopedDecoratorData(context, internalKey, entity) ?? false;
if (entity.kind === "Operation" || found) {
return found;
Expand Down Expand Up @@ -722,13 +728,13 @@ export function $usage(
}

export function getUsageOverride(
context: SdkContext,
context: TCGCContext,
entity: Model | Enum
): UsageFlags | undefined {
return getScopedDecoratorData(context, usageKey, entity);
}

export function getUsage(context: SdkContext, entity: Model | Enum): UsageFlags {
export function getUsage(context: TCGCContext, entity: Model | Enum): UsageFlags {
if (!context.modelsMap) {
getAllModels(context); // this will populate modelsMap
}
Expand Down Expand Up @@ -756,14 +762,14 @@ export function $access(
}

export function getAccessOverride(
context: SdkContext,
context: TCGCContext,
entity: Model | Enum | Operation
): AccessFlags | undefined {
return getScopedDecoratorData(context, accessKey, entity);
}

export function getAccess(
context: SdkContext,
context: TCGCContext,
entity: Model | Enum | Operation
): AccessFlags | undefined {
const override = getScopedDecoratorData(context, accessKey, entity);
Expand Down Expand Up @@ -800,11 +806,11 @@ export function $flattenProperty(
/**
* Whether a model property should be flattened or not.
*
* @param context SdkContext
* @param context TCGCContext
* @param target ModelProperty that we want to check whether it should be flattened or not
* @returns whether the model property should be flattened or not
*/
export function shouldFlattenProperty(context: SdkContext, target: ModelProperty): boolean {
export function shouldFlattenProperty(context: TCGCContext, target: ModelProperty): boolean {
return getScopedDecoratorData(context, flattenPropertyKey, target) ?? false;
}

Expand All @@ -819,6 +825,6 @@ export function $clientName(
setScopedDecoratorData(context, $clientName, clientNameKey, entity, value, scope);
}

export function getClientNameOverride(context: SdkContext, entity: Type): string | undefined {
export function getClientNameOverride(context: TCGCContext, entity: Type): string | undefined {
return getScopedDecoratorData(context, clientNameKey, entity);
}
15 changes: 2 additions & 13 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
Interface,
ModelProperty,
Namespace,
Operation,
Program,
Type,
UsageFlags,
} from "@typespec/compiler";
Expand All @@ -18,6 +16,7 @@ import {
HttpVerb,
Visibility,
} from "@typespec/http";
import { TCGCContext } from "./internal-utils.js";

export type SdkParameterLocation =
| "endpointPath"
Expand All @@ -28,18 +27,8 @@ export type SdkParameterLocation =
| "unknown";
export type SdkParameterImplementation = "Client" | "Method";

export interface SdkContext<TOptions extends object = Record<string, any>> {
program: Program;
export interface SdkContext<TOptions extends object = Record<string, any>> extends TCGCContext {
emitContext: EmitContext<TOptions>;
emitterName: string;
generateProtocolMethods: boolean;
generateConvenienceMethods: boolean;
filterOutCoreModels?: boolean;
packageName?: string;
modelsMap?: Map<Type, SdkModelType | SdkEnumType>;
operationModelsMap?: Map<Operation, Map<Type, SdkModelType | SdkEnumType>>;
generatedNames?: Set<string>;
arm?: boolean;
}

export interface SdkEmitterOptions {
Expand Down
23 changes: 23 additions & 0 deletions packages/typespec-client-generator-core/src/internal-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Operation, Program, Type } from "@typespec/compiler";
import { SdkEnumType, SdkModelType } from "./interfaces.js";

export function parseEmitterName(emitterName?: string): string {
if (!emitterName) {
throw new Error("No emitter name found in program");
Expand All @@ -9,3 +12,23 @@ export function parseEmitterName(emitterName?: string): string {
if (["typescript", "ts"].includes(language)) return "javascript";
return language;
}

export interface TCGCContext {
program: Program;
emitterName: string;
generateProtocolMethods?: boolean;
generateConvenienceMethods?: boolean;
filterOutCoreModels?: boolean;
packageName?: string;
arm?: boolean;
modelsMap?: Map<Type, SdkModelType | SdkEnumType>;
operationModelsMap?: Map<Operation, Map<Type, SdkModelType | SdkEnumType>>;
generatedNames?: Set<string>;
}

export function createTCGCContext(program: Program): TCGCContext {
return {
program,
emitterName: "__TCGC_INTERNAL__",
};
}
Loading

0 comments on commit dac6668

Please sign in to comment.