TypeSpec Type Representation in TCGC
This page documents what type definitions in TypeSpec would look like when returned by TCGC
Main TypeSpec Code Example
import "@typespec/http";
using TypeSpec.Http;
@useAuth(ApiKeyAuth<ApiKeyLocation.header, "api-key">)@service({ title: "Contoso Widget Manager",})@server( "{endpoint}/widget", "Contoso Widget APIs", { endpoint: string, })namespace Contoso.WidgetManager;
model Widget { @visibility("read", "update") @path id: string;
weight: int32; color: "red" | "blue";}
@errormodel Error { code: int32; message: string;}
@route("/widgets")@tag("Widgets")interface Widgets { @get list(): Widget[] | Error; @get read(@path id: string): Widget | Error; @post create(...Widget): Widget | Error; @patch update(...Widget): Widget | Error; @delete delete(@path id: string): void | Error; @route("{id}/analyze") @post analyze(@path id: string): string | Error;}
Overall Hierarchy of Types
This is a rough sketch of the hierarchy. We’ll go into more detail for each section, this is just to get an idea of the overall structure.
const sdkContext = { sdkPackage: { clients: [ { kind: "client", name: "WidgetClient", initialization: { properties: [ { kind: "endpoint", type: { kind: "union", values: [ { kind: "endpoint", serverUrl: "{endpoint}", templateArguments: [ { name: "endpoint", kind: "path", ... } ] }, { kind: "endpoint", serverUrl: "{endpoint}/widget", templateArguments: [ { name: "endpoint", kind: "path", ... } ] } ] } }, { kind: "credential", ... } ] }, methods: [ { kind: "clientaccessor", response: { name: "WidgetPartsClient", kind: "client", ... } }, { kind: "basic", parameters: [ { name: "id", kind: "method", type: { kind: "string", }, ... } ], operation: { kind: "http", verb: "get", parameters: [ { name: "id", kind: "path", type: { kind: "string", }, correspondingMethodParams: [ { name: "id", kind: "method", type: { kind: "string", }, ... } ] } ], bodyParam: undefined, responses: { 200: { kind: "http", ... } }, examples: [ { kind: "http", name: "Example 1", description: "Example 1", filePath: "example1.json", rawExample: {...}, parameters: [...], responses: {...} } ] }, response: { kind: "method", logicalPath: undefined, ... }, exception: { kind: "method", ... }, ... } ], ... } ], models: [ { kind: "model", name: "Widget", properties: [ { kind: "path", name: "id", type: { kind: "string", }, ... }, { kind: "property", name: "color", type: { kind: "enum", isFixed: true, ... }, ... } ] } ], enums: [ { name: "WidgetColor", generatedName: true, values: [ { value: "red", ... }, { value: "blue", ... } ] } ], ... }, ...}
SdkContext
SdkContext
is the context of your SDK emitter.
Interface
export interface SdkContext< TOptions extends object = Record<string, any>, TServiceOperation extends SdkServiceOperation = SdkHttpOperation,> { // emit context from tsp that is used to create our TCGC Sdk Context emitContext: EmitContext<TOptions>; // representation of the entire package the language emitter should generate sdkPackage: SdkPackage<TServiceOperation>; program: Program; emitterName: string;}
Usage
It is used to persist information across functions. You should never define global constants or variables in your emitter code, because this leads to issues with multiple emitter runs. Instead, all global information should be stored in your context.
You should define your own language-specific SdkContext
that extends from the one defined in @azure-tools/typespec-client-generator-core
. This is where you keep your global variables. You should utilize the createSdkContext
from tcgc.
You also shouldn’t instantiate your own interface, instead you should call createSdkContext
to create.
import { EmitContext } from "@typespec/compiler";import { createSdkContext, SdkServiceOperation } from "@azure-tools/typespec-client-generator-core";import { PythonEmitterOptions } from "./lib.js";
interface PythonSdkContext<TServiceOperation extends SdkServiceOperation> extends SdkContext<PythonEmitterOptions, TServiceOperation> { ClientMap: Map<SdkClientType, PythonSdkClientType>;}
export async function $onEmit(context: EmitContext<PythonEmitterOptions>) { const sdkContext: PythonSdkContext = { ...createSdkContext(context, "@azure-tools/typespec-python"), ClientMap: new Map<sdkClientType, PythonSdkClientType>(), };}
SdkPackage
SdkPackage
represents the entire package that your language emitter should generate. It synthesizes all of the information language emitters will need to generate the entire package
Interface
export interface SdkPackage<TServiceOperation extends SdkServiceOperation> { name: string; rootNamespace: string; clients: SdkClientType<TServiceOperation>[]; models: SdkModelType[]; enums: SdkEnumType[]; diagnostics: readonly Diagnostic[];}
Example
const sdkPackage = { name: "ContosoWidgetManager", rootNamespace: "Contoso.WidgetManager", clients: [ { name: "ContosoWidgetManagerClient", kind: "client", ... } ], models: [ { name: "Widget", kind: "model", ... }, { name: "Error", kind: "model", ... } ], enums: [], diagnostics: [],}
Usage
const sdkPackage = sdkContext.sdkPackage;
SdkClientType
An SdkClientType
represents a single client in your package.
All clients will have an initialization
property. Whether that property’s access is internal
or public
will determine whether your client can be publicly instantiated or not.
Interface
export interface SdkClientType<TServiceOperation extends SdkServiceOperation> { name: string; kind: "client"; description?: string; details?: string; initialization: SdkInitializationType; apiVersions: string[]; // fully qualified. Compare to sdkPackage.rootNamespace to figure out where to generate nameSpace: string; methods: SdkMethod<TServiceOperation>[];}
Example
const sdkClient = { name: "ContosoWidgetManagerClient"; kind: "client"; initialization: { name: "WidgetManagerOptions", kind: "model", access: "public", ... }; apiVersions: [], nameSpace: "Contoso.WidgetManager", methods: [ { name: "getWidgets", kind: "clientaccessor", response: { name: "Widgets", kind: "client", initialization: { kind: "model", access: "internal", }, apiVersions: [], nameSpace: "Contoso.WidgetManager", methods: [ { name: "list", kind: "method", ... }, { name: "read", kind: "method", ... } ] } } ],}
Usage
import { SdkClientType } from "@azure-tools/typespec-client-generator-core";import { PythonSdkServiceOperation } from "./interfaces.js";import { get }
const serializedClients: PythonSdkClientType[] = [];
function serializeClient< TServiceOperation extends PythonSdkServiceOperation>( sdkContext: PythonSdkContext<TServiceOperation>, client: SdkClientType<TServiceOperation>,): PythonSdkClientType { // Map the information from the SdkClientType to your language's interface for a client. // Would recommend that eventually your language's client type interface extends from // SdkClientType, and just adds your language-specific information on top. const pythonClient = { ...client, parameters: (client.initialization.properties.map( (x) => getSdkModelPropertyType( context, x ) ) : undefined ), methods: client.methods.filter( (x) => x.kind === "method" ).map( (x) => serializeServiceMethod(sdkContext, x) ), subclients: [], } sdkContext.ClientMap.set(client, pythonClient) return pythonClient}
for (const client of sdkPackage.clients) { serializedClients.push(serializeClient(sdkContext, client));}
SdkInitializationType
Initialization model for a client. Whether it’s access
is public
or internal
tells you whether the client is publicly instantiable.
Interface
export interface SdkInitializationType extends SdkModelType { // properties takes care of all of the initialization info that you will need. properties: (SdkEndpointParameter | SdkCredentialParameter | SdkMethodParameter)[];}
Example
const sdkInitializationType = { name: "WidgetManagerOptions", kind: "model", properties: [ { kind: "endpoint", serverUrl: "{endpoint}/widget", templateArguments: [ { kind: "path", name: "endpoint", type: { kind: "string", }, }, ], }, { kind: "credential", type: { kind: "credential", scheme: { kind: "apiKey", in: "header", name: "api-key", }, }, onClient: true, }, ], crossLanguageDefinitionId: "Contoso.WidgetManager.WidgetManagerOptions", apiVersions: [], usage: UsageFlags.Input, access: "public", isFormDataType: false, isError: false,};
Usage
The usage of this property is more language-dependent.
Some emitters will create a model and have the client accept the model as options to initialize your client. Others will flatten out the parameters and accept them individually as parameter input.
SdkMethod
An SdkMethod
is any of the types of methods that can be a method on a client.
There are two main types of an SdkMethod
:
SdkClientAccessor
returns a subclient of the client it’s onSdkServiceMethod
wraps a service call
export type SdkMethod<TServiceOperation extends PythonSdkServiceOperation> = | SdkServiceMethod<TServiceOperation> | SdkClientAccessor<TServiceOperation>;
They each extend from the shared SdkMethodBase
interface SdkMethodBase<TServiceOperation extends SdkServiceOperation> { __raw?: Operation; name: string; access: AccessFlags; parameters: SdkParameter[]; apiVersions: string[]; description?: string; details?: string;}
Below we go into each method type
SdkClientAccessor
A clientaccessor
method is simply a method on a client that returns another client. The returned client can be instantiable or un-instantiable. If the returned client is instantiable, most likely your clientaccessor
will be part of the public api surface, so users can instantiate the subclient using your client method. If it is not instantiable, it is up to you how to expose the subclient on your current client for users to access.
Interface
interface SdkClientAccessor<TServiceOperation extends SdkServiceOperation> extends SdkMethodBase<TServiceOperation> { kind: "clientaccessor"; response: SdkClientType<TServiceOperation>;}
Example
const sdkClientAccessor = { name: "getWidgets", kind: "clientaccessor", response: { name: "Widgets", kind: "client", initialization: undefined, ... }}
Usage
Our strong recommendation is you build up a list of clients by iterating through sdkPackage.clients
first, and to not recurse into creating the subclients.
Only once you’ve processed each client, then you go through and link clients to their subclients. Not doing recursion prevents confusion over where you are in the call chain.
import { SdkServiceOperation, SdkClientType } from "@azure-tools/typespec-client-generator-core";import { PythonSdkContext } from "./lib.js";import { PythonSdkClientType } from "./interfaces.js";
function linkSubClients<TServiceOperation extends PythonSdkServiceOperation>( sdkContext: PythonSdkContext<TServiceOperation>,): void { for (const client of sdkContext.clients) { client.subclients = client.methods .filter((x) => x.kind === "clientaccessor") .map((x) => x.returnType) .map((x) => sdkContext.ClientMap.get(x)); }}
SdkServiceMethod
An SdkServiceMethod
is any service method on a client that calls the service.
The actual service call is a separate property on the method, .operation
. This way, our service methods are able to abstract away the protocol used to call the service (i.e. http
or gRPC
)
All SdkServiceMethod
s share the following base:
interface SdkServiceMethodBase<TServiceOperation extends SdkServiceOperation> extends SdkMethodBase<TServiceOperation> { // This represents the operation request we need to send to the service. // This helps abstract away the transport operation: TServiceOperation; // The parameters you generate on the method signature that users will interact with. // These aren't tied to any protocol, so there are no 'header' / 'path' parameters etc. parameters: SdkMethodParameter[]; response: SdkMethodResponse; exception?: SdkMethodResponse;}
function serializeServiceMethod<TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkServiceMethod<TServiceOperation>,): PythonSdkServiceMethod { switch (method.kind) { case "basic": return serializeBasicServiceMethod(context, method); case "paging": return serializePagingServiceMethod(context, method); case "lro": return serializeLroServiceMethod(context, method); case "lropaging": return serializeLroPagingServiceMethod(context, method); }}
SdkBasicServiceMethod
This models a basic service call that is synchronous server side.
Interface
export interface SdkBasicServiceMethod<TServiceOperation extends SdkServiceOperation> extends SdkServiceMethodBase<TServiceOperation> { kind: "basic";}
Example
const sdkBasicServiceMethod = { kind: "basic", __raw?: { // Original Tsp Operation type kind: "Operation", }, name: "read", access: "public", apiVersions: [], parameters: [ { name: "id", kind: "method", ... }, ], operation: { kind: "http", ... }, response: { kind: "method", type: { kind: "model", name: "Widget", } },}
Usage
function serializeBasicServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkBasicServiceMethod<TServiceOperation>,): PythonSdkBasicServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method, method.operation) },}
SdkPagingServiceMethod
This represents a paging method we will generate on the client. It includes an initial service operation call, and potentially a next link operation as well.
We currently only have paging method support for azure-generated sdks.
Interface
export interface SdkPagingServiceMethod<TServiceOperation extends SdkServiceOperation> extends SdkServiceMethodBase<TServiceOperation> { kind: "paging"; // raw paging information returned from getPagedResult __raw_paged_metadata: PagedResultMetadata; // string to get to next link. If undefined, we are doing fake paging nextLinkPath?: string; // service operation if separate requests need to be made for subsequent paging nextLinkOperation?: TServiceOperation;}
Example
import "@azure-tools/typespec-azure-core";
using Azure.Core;using Azure.Core.Traits;
// Skipping past service definitions
alias Operations = Azure.Core.ResourceOperations<SupportsRepeatableRequests>;
listWidgets is Operations.ResourceList<Widget>;
const sdkPagingServiceMethod = { kind: "paging", __raw?: { // Original Tsp Operation type kind: "Operation", }, __raw_paged_metadata: { // Raw output from getPagedMetadata } name: "listWidgets", access: "public", apiVersions: [], parameters: [], operation: { kind: "http", ... }, response: { kind: "method", type: { kind: "model", name: "Widget", }, // we want to return the `.value` part of the service response to users pathFromService: "value", }, nextLinkPath: "nextLink", nextLinkOperation: undefined,}
Usage
function serializePagingServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkPagingServiceMethod<TServiceOperation>,): PythonSdkBasicServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method, method.operation), nextLinkOperation: method.nextLinkOperation ? serializeServiceOperation(context, method, method.nextLinkOperation) : undefined, },}
SdkLroServiceMethod
Represents an LRO method we want to generate on the client.
Only returned for azure-generated sdks
Interfaces
export interface SdkLroServiceMethod<TServiceOperation extends SdkServiceOperation> extends SdkServiceMethodBase<TServiceOperation> { kind: "lro"; // raw LroMetadata returned from azure-core helper function getLroMetadata __raw_lro_metadata: LroMetadata; // initial call to begin LRO polling // thinking of instead making it the regular operation property so we can continue using that property operation: TServiceOperation;}
Example
import "@azure-tools/typespec-azure-core";
using Azure.Core;using Azure.Core.Traits;
// Skipping past service definitions
alias Operations = Azure.Core.ResourceOperations<SupportsRepeatableRequests>;
// Operation Status/** Gets status of a Widget operation. */getWidgetOperationStatus is Operations.GetResourceOperationStatus<Widget>;
// Widget Operations/** Creates or updates a Widget asynchronously */@pollingOperation(getWidgetOperationStatus)createOrUpdateWidget is Operations.LongRunningResourceCreateOrUpdate<Widget>;
const sdkLroServiceMethod = { kind: "lro", __raw?: { // Original Tsp Operation type kind: "Operation", }, __raw_lro_metadata: { // Raw output from getLroMetadata } name: "createOrUpdateWidget", access: "public", apiVersions: [], parameters: [], operation: { kind: "http", ... }, response: { kind: "method", type: { kind: "model", name: "Widget", } },}
Usage
function serializeLroServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkLroServiceMethod<TServiceOperation>,): PythonSdkLroServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method), },}
SdkLroPagingServiceMethod
This is a combination of Lro and paging. We start off with an LRO call to the service, and then the response is returned to us in pages. Also only available for azure-sdks.
Interface
export interface SdkLroPagingServiceMethod<TServiceOperation extends SdkServiceOperation> extends SdkServiceMethodBase<TServiceOperation> { kind: "lropaging"; __raw_lro_metadata: LroMetadata; // initial call to start LRO operation: SdkServiceOperation; __raw_paged_metadata: PagedResultMetadata; nextLinkPath?: string; // off means fake paging nextLinkOperation?: SdkServiceOperation;}
Usage
function serializeLroPagingServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkLroPagingServiceMethod<TServiceOperation>,): PythonSdkLroPagingServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method, method.operation), nextLinkOperation: method.nextLinkOperation ? serializeServiceOperation(context, method, method.nextLinkOperation) : undefined, },}
SdkServiceOperation
One main part of this design is we’ve decoupled the actual service operation call from the method we generate on our client. This is because we want to abstract away the protocol used to call our service. Additionally, while there is a high level of correlation between the method parameters we intake from SDK users, and the parameters we end up passing the service, it’s not one-to-one. We need to serialize the method parameters and additionally, when parameters are spread or grouped together, it requires us to do mapping between which method parameters correspond to which service parameters.
We currently only support HTTP calls to the service.
Interface
export interface SdkHttpOperation { kind: "http"; // raw HTTP operation output from typespec/http __raw: HttpOperation; // route path for the target operation. TypeSpec suggest to use `uriTemplate` instead as `path` will not work for complex cases like not-escaping reserved chars. path: string; // the fully resolved URI template for the target operation as defined by [RFC 6570](https://datatracker.ietf.org/doc/html/rfc6570). uriTemplate: string; verb: HttpVerb; parameters: (SdkPathParameter | SdkQueryParameter | SdkHeaderParameter)[]; bodyParam: SdkBodyParameter; // mapping of status codes to SdkHttpResponse for valid responses // HttpStatusCodeRange can represent either a single status code or a range. responses: SdkHttpResponse[]; exceptions: SdkHttpResponse[]; examples?: SdkHttpOperationExample[];}
Example
const sdkHttpOperation = { kind: "http", __raw: { // raw HttpOperation from @typespec/http; }, path: "/widgets", uriTemplate: "/widgets", verb: "get", parameters: [ { kind: "path", name: "id", ... } ], bodyParam: undefined, responses: { 200: { kind: "http", ... }, }, exceptions: { "*": { kind: "http", ... } }, examples: [ { kind: "http", name: "Example 1", description: "Example 1", filePath: "example1.json", rawExample: {...}, parameters: [...], responses: {...} } ]}
Usage
function serializeServiceOperation<TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkServiceMethod<TServiceOperation>, operation: TServiceOperation,): TServiceOperation { switch (operation.kind) { case "http": return serializeHttpServiceOperation(context, method, operation); }}
Parameters and Properties
Like the TypeSpec concept of a ModelProperty
, all properties and parameters are part of the SdkModelPropertyType
. They all extend from SdkModelPropertyTypeBase
.
export type SdkModelPropertyType = | SdkMethodParameter | SdkEndpointParameter | SdkCredentialParameter | SdkBodyModelPropertyType | SdkQueryParameter | SdkPathParameter | SdkHeaderParameter | SdkBodyParameter;
interface SdkModelPropertyTypeBase { __raw?: ModelProperty; type: SdkType; name: string; description?: string; details?: string; apiVersions: string[]; onClient: boolean; // clientDefaultValue only exists for api versions clientDefaultValue?: any; isApiVersionParam: boolean; optional: boolean; nullable: boolean;}
Method parameters
These are parameters to client initialization and method son the client. These will be the parameters that SDK users directly use. They will eventually be mapped to
SdkEndpointParameter
An SdkEndpointParameter
represents a parameter to a client’s endpoint.
TCGC will always give it to you as overridable:
If the server URL is a constant, we will return a templated endpoint with a default value of the constant server URL. In the case where the endpoint has extra template arguments, the type is a union of a completely-overridable endpoint, and an endpoint that accepts template arguments. If there are multiple servers, we will return the union of all of the possibilities.
export interface SdkEndpointParameter extends SdkModelPropertyTypeBase { kind: "endpoint"; urlEncode: boolean; onClient: true; serializedName?: string; type: SdkEndpointType | SdkUnionType<SdkEndpointType>;}
SdkCredentialParameter
Parameter for credential input to clients.
export interface SdkCredentialParameter extends SdkModelPropertyTypeBase { kind: "credential"; // can be either an SdkCredentialType or a union // of different credential types type: SdkCredentialType | SdkUnionType; onClient: true;}
SdkMethodParameter
Represents a parameter to a client method. Does not have any transport (i.e. HTTP) related information on it. This is solely meant to represent information that we expect sdk users to pass in. We then take care to map the method input and create our request to the service using the information users have inputted.
export interface SdkMethodParameter extends SdkModelPropertyTypeBase { kind: "method";}
Service Parameters
Currently we only support HTTP service parameters.
SdkQueryParameter
This represents an HTTP query parameter.
// different collection format to use to separate each value in array// - multi: no separator, treat each value in array as a separate query parameter// - csv: , as separator// - ssv: space as separator// - tsv: tab as separator// - pipes: | as separator// - simple: , as separator// - form: no separator, treat each value in array as a separate query parameterexport type CollectionFormat = "multi" | "csv" | "ssv" | "tsv" | "pipes" | "simple" | "form";
export interface SdkQueryParameter extends SdkModelPropertyTypeBase { kind: "query"; // the collection format to use to separate each value in array of a query parameter collectionFormat?: CollectionFormat; serializedName: string; correspondingMethodParams: SdkModelPropertyType[]; // if true, send each value in array or object as a separate query parameter, default is `false` explode: boolean;}
SdkHeaderParameter
This is an HTTP header parameter.
// different collection format to use to separate each value in array// - multi: no separator, treat each value in array as a separate header parameter// - csv: , as separator// - ssv: space as separator// - tsv: tab as separator// - pipes: | as separator// - simple: , as separator// - form: no separator, treat each value in array as a separate header parameterexport type CollectionFormat = "multi" | "csv" | "ssv" | "tsv" | "pipes" | "simple" | "form";
export interface SdkHeaderParameter extends SdkModelPropertyTypeBase { kind: "header"; collectionFormat?: CollectionFormat; serializedName: string; correspondingMethodParams: SdkModelPropertyType[];}
SdkPathParameter
export interface SdkPathParameter extends SdkModelPropertyTypeBase { kind: "path"; // when interpolating this parameter in the case of array or object, expand each value using the given style, default is `false` explode: boolean; // different interpolating styles for the path parameter, default is `"simple"` // - simple: no separator // - label: . as separator // - matrix: ; as separator // - fragment: # as separator // - path: / as separator style: "simple" | "label" | "matrix" | "fragment" | "path"; // when interpolating this parameter, do not encode reserved characters, default is `false` allowReserved: boolean; serializedName: string; optional: false; correspondingMethodParams: SdkModelPropertyType[];}
SdkBodyParameter
export interface SdkBodyParameter extends SdkModelPropertyTypeBase { kind: "body"; optional: boolean; contentTypes: string[]; defaultContentType: string; correspondingMethodParams: SdkModelPropertyType[];}
Properties
SdkBodyModelPropertyType
This represents a property on a body model
export interface SdkBodyModelPropertyType extends SdkModelPropertyTypeBase { kind: "property"; discriminator: boolean; serializedName: string; isMultipartFileInput: boolean; visibility: Visibility[]; flatten: boolean;}
Usage
We recommend that for usage, you use one single function with switch statements for each kind of property.
This allows for the circular nature of an SdkModelPropertyType
.
import { SdkServiceOperation, SdkModelPropertyType } from "@azure-tools/typespec-client-generator-core";import { PythonSdkContext } from "./lib.js";import { getPythonSdkType } from "./types.js";
export function getSdkModelPropertyType< TServiceOperation extends SdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, prop: SdkModelPropertyType): PythonSdkModelType { const type = getPythonSdkType(context, prop.type) switch (prop.kind) { case "query": case "header" case "path": return { ...prop, type, correspondingMethodParams: prop.correspondingMethodParams.map( (x) => getSdkModelPropertyType(context, x), ) } default: return { ...prop, type, } }}
SdkMethodResponse
This represents the response that our method will ultimately return. It is not tied to a transport, like http, it solely represents what we want to return to users
Interface
export interface SdkMethodResponse { kind: "method"; // the path from the service response to what the method returns. logicalPath?: string; type?: SdkType; description?: string; details?: string;}
Example
const sdkMethodResponse = { kind: "method", logicalPath: undefined, type: { kind: string, },}
const sdkPagingMethodResponse = { kind: "method", logicalPath: "value", type: { kind: "array", valueType: { kind: "model", name: "Widget", nullableValues: false, ... } }}
Usage
import { SdkMethodResponse } from "@azure-tools/typespec-client-generator-core";import { PythonSdkContext, PythonSdkMethodResponse } from "./lib.js";import { getPythonSdkType } from "./types.js";
function serializeMethodResponse<TServiceOperation extends SdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, response: SdkMethodResponse,): PythonSdkMethodResponse { return { ...response, type: getPythonSdkType(context, response.type), };}
SdkHttpResponse
This is the response returned from an HTTP service. The emitters should take care to map it to what the method ultimately returns to end users.
Each response is mapped to an HttpStatusCodeRange
on the SdkHttpOperation
interface.
Interface
interface HttpStatusCodeRange { start: number; // inclusive start. If there's only start, then it's just a single value. end?: number; // exclusive end. If it's a range, end is present, otherwise it's undefined.}
export interface SdkHttpResponse { kind: "http"; statusCodes: number | HttpStatusCodeRange | "*"; headers: SdkServiceResponseHeader[]; apiVersions: string[]; type?: SdkType; contentTypes: string[]; defaultContentType?: string;}
SdkType
These are the TCGC versions of each TypeSpec Type. They include information that all emitters want, i.e. their description directly on them.
They all share this base:
interface SdkTypeBase { // the raw TypeSpec type. Some of our types are currently // created by tcgc. Those won't have an original type __raw?: Type; kind: string; deprecation?: string; description?: string; details?: string;}
SdkBuiltInType
A SdkBuiltInType
represents a built-in scalar TypeSpec type or scalar type that derives from a built-in scalar TypeSpec type, but datetime and duration are not included.
We add encode
onto these types if @encode
decorator exists, telling us how to encode when sending to the service.
There is a one-to-one mapping between the TypeSpec scalar kinds and the SdkBuiltInKinds
.
Interface
export interface SdkBuiltInType extends SdkTypeBase { kind: SdkBuiltInKinds; name: string; encode?: string; baseType?: SdkBuiltInType; crossLanguageDefinitionId: string;}
The crossLanguageDefinitionId
represents the fully qualified name of this type in TypeSpec language for the emitter to distinguish from the built-in TypeSpec types.
For a full list of types defined in @azure-tools/typespec-azure-core
library, please refer to its reference doc.
SdkDateTimeType
interface SdkDatetimeTypeBase extends SdkTypeBase { name: string; baseType?: SdkDateTimeType; encode: DateTimeKnownEncoding; // what we send over the wire. Often it's string wireType: SdkBuiltInType; crossLanguageDefinitionId: string;}
interface SdkUtcDatetimeType extends SdkDatetimeTypeBase { kind: "utcDateTime";}
interface SdkOffsetDatetimeType extends SdkDatetimeTypeBase { kind: "offsetDateTime";}
SdkDurationType
interface SdkDurationType extends SdkTypeBase { kind: "duration"; name: string; baseType?: SdkDurationType; encode: DurationKnownEncoding; // What we send over the wire. It's usually either a string or a float wireType: SdkBuiltInType; crossLanguageDefinitionId: string;}
SdkArrayType
interface SdkArrayType extends SdkTypeBase { kind: "array"; name: string; valueType: SdkType; crossLanguageDefinitionId: string;}
SdkDictionaryType
interface SdkDictionaryType extends SdkTypeBase { kind: "dict"; keyType: SdkType; // currently can only be string valueType: SdkType;}
SdkEnumType
export interface SdkEnumType extends SdkTypeBase { kind: "enum"; name: string; // Determines whether the name was generated or not isGeneratedName: boolean; valueType: SdkBuiltInType; values: SdkEnumValueType[]; isFixed: boolean; isFlags: boolean; usage: UsageFlags; access: AccessFlags; crossLanguageDefinitionId: string; apiVersions: string[]; isUnionAsEnum: boolean;}
SdkEnumValueType
export interface SdkEnumValueType extends SdkTypeBase { kind: "enumvalue"; name: string; value: string | number; enumType: SdkEnumType; valueType: SdkBuiltInType;}
SdkConstantType
export interface SdkConstantType extends SdkTypeBase { kind: "constant"; value: string | number | boolean | null; valueType: SdkBuiltInType; name: string; isGeneratedName: boolean;}
SdkUnionType
export interface SdkUnionType extends SdkTypeBase { name: string; // determines if the union name was generated or not isGeneratedName: boolean; kind: "union"; variantTypes: SdkType[]; crossLanguageDefinitionId: string;}
SdkTupleType
export interface SdkTupleType extends SdkTypeBase { kind: "tuple"; valueTypes: SdkType[];}
SdkModelType
export interface SdkModelType extends SdkTypeBase { kind: "model"; // purposely can also be header / query params for fidelity with TypeSpec properties: SdkModelPropertyType[]; // we will always have a name. generatedName determines if it's generated or not. name: string; isGeneratedName: boolean; access: AccessFlags; usage: UsageFlags; additionalProperties?: SdkType; discriminatorValue?: string; discriminatedSubtypes?: Record<string, SdkModelType>; discriminatorProperty?: SdkModelPropertyType; baseModel?: SdkModelType; crossLanguageDefinitionId: string; apiVersions: string[];}
UsageFlags
export enum UsageFlags { None = 0, Input = 1 << 1, Output = 1 << 2, ApiVersionEnum = 1 << 3, JsonMergePatch = 1 << 4, MultipartFormData = 1 << 5,}
AccessFlags
We default the value of .access
property on model, enum, and method types to be "public"
. So if the @access
decorator isn’t explicitly applied to one of these definitions, its value will be "public"
.
If you want to know if a tsp author explicitly set the value with an @access
decorator, you can call getAccessOverride
export type AccessFlags = "internal" | "public";
SdkEndpointType
export interface SdkEndpointType { kind: "endpoint"; serverUrl: string; templateArguments: SdkPathParameter[];}
Example Types
The example types help to model the examples that TypeSpec author defined to help user understand how to use the API.
We currently only have examples based on the payload, so the examples model will bind to the SdkServiceOperation.
Operation Related Interface
These types are used to represent an example of a service operation.
We currently only support HTTP calls to the service.
So, we have SdkHttpoperationExample
bind to SdkHttpOperation
, SdkHttpParameterExampleValue
bind to SdkHttpParameter
, SdkHttpResponseExampleValue
bind to SdkHttpResponse
, and SdkHttpResponseHeaderExampleValue
bind to SdkHttpResponseHeader
.
Each type will have the example value type and its cooresponding definition type.
interface SdkExampleBase { kind: string; name: string; description: string; filePath: string; rawExample: any;}
export interface SdkHttpOperationExample extends SdkExampleBase { kind: "http"; parameters: SdkHttpParameterExampleValue[]; responses: SdkHttpResponseExampleValue[];}
export interface SdkHttpParameterExampleValue { parameter: SdkHttpParameter; value: SdkExampleValue;}
export interface SdkHttpResponseExampleValue { response: SdkHttpResponse; statusCode: number; headers: SdkHttpResponseHeaderExampleValue[]; bodyValue?: SdkExampleValue;}
export interface SdkHttpResponseHeaderExampleValue { header: SdkServiceResponseHeader; value: SdkExampleValue;}
SdkExampleValue
These types are used to represent the example value of a type. One definition types will have different example value types.
For SdkUnionExampleValue
, since it is hard to determine whether the example value should belong to which union variant, we will keep the raw value and leave the work for the emitter.
For SdkModelExampleValue
, we will help to map the example type to the right subtype for the discriminated type, and we will separate the additional properties value from the property value.
But for the model with inheritance, we will not break down the type graph, just put all the example value in the child model.
export type SdkExampleValue = | SdkStringExampleValue | SdkNumberExampleValue | SdkBooleanExampleValue | SdkNullExampleValue | SdkUnknownExampleValue | SdkArrayExampleValue | SdkDictionaryExampleValue | SdkUnionExampleValue | SdkModelExampleValue;
interface SdkExampleValueBase { kind: string; type: SdkType; value: unknown;}
export interface SdkStringExampleValue extends SdkExampleTypeBase { kind: "string"; type: | SdkBuiltInType | SdkDateTimeType | SdkDurationType | SdkEnumType | SdkEnumValueType | SdkConstantType; value: string;}
export interface SdkNumberExampleValue extends SdkExampleTypeBase { kind: "number"; type: | SdkBuiltInType | SdkDateTimeType | SdkDurationType | SdkEnumType | SdkEnumValueType | SdkConstantType; value: number;}
export interface SdkBooleanExampleValue extends SdkExampleTypeBase { kind: "boolean"; type: SdkBuiltInType | SdkConstantType; value: boolean;}
export interface SdkNullExampleValue extends SdkExampleTypeBase { kind: "null"; type: SdkNullableType; value: null;}
export interface SdkUnknownExampleValue extends SdkExampleTypeBase { kind: "unknown"; type: SdkBuiltInType; value: unknown;}
export interface SdkArrayExampleValue extends SdkExampleTypeBase { kind: "array"; type: SdkArrayType; value: SdkExampleValue[];}
export interface SdkDictionaryExampleValue extends SdkExampleTypeBase { kind: "dict"; type: SdkDictionaryType; value: Record<string, SdkExampleValue>;}
export interface SdkUnionExampleValue extends SdkExampleTypeBase { kind: "union"; type: SdkUnionType; value: unknown;}
export interface SdkModelExampleValue extends SdkExampleTypeBase { kind: "model"; type: SdkModelType; value: Record<string, SdkExampleValue>; additionalPropertiesValue?: Record<string, SdkExampleValue>;}
Example
const sdkHttpOperationExample = { kind: "http", name: "Example 1", description: "Example 1", filePath: "example1.json", rawExample: { operationId: "Widgets_Read", title: "Example 1", parameters: { id: "test", }, responses: { 200: { body: { id: "test", color: "red", weight: 100, }, }, }, }, parameters: [ { parameter: {}, // ref to the SdkHttpParameter value: { kind: "string", type: {}, // ref to the SdkType value: "test", }, }, ], responses: { 200: { response: {}, // ref to the SdkHttpResponse bodyValue: { kind: "model", type: {}, // ref to the SdkType value: { id: { kind: "string", type: {}, // ref to the SdkType value: "test", }, color: { kind: "string", type: {}, // ref to the SdkType value: "red", }, weight: { kind: "number", type: {}, // ref to the SdkType value: 100, }, }, }, }, },};
Usage
function serializeServiceOperationExample( context: PythonSdkContext<SdkHttpOperation>, operation: SdkHttpOperation,): PythonSdkHttpOperationExample { for (const example of operation.examples) { return { name: example.name, filePath: example.filePath, parameters: example.parameters.map((x) => ({ parameter: getSdkModelPropertyType(x.parameter), value: serializeTypeExample(context, x.value), })), responses: new Map( [...example.responses].map(([key, value]) => [ key, { response: serializeHttpResponse(value.response), headers: value.headers.map((x) => ({ header: getSdkModelPropertyType(x.header), value: serializeTypeExample(context, x.value), })), bodyValue: serializeTypeExample(context, value.bodyValue), }, ]), ), }; }}
function serializeTypeExample( context: PythonSdkContext<SdkHttpOperation>, example: SdkExampleValue,): PythonSdkTypeExample { switch (example.kind) { case "string": return { ...example, type: getPythonSdkType(context, example.type), }; case "number": return { ...example, type: getPythonSdkType(context, example.type), }; case "boolean": return { ...example, type: getPythonSdkType(context, example.type), }; case "null": return { ...example, type: getPythonSdkType(context, example.type), }; case "unknown": return { ...example, type: getPythonSdkType(context, example.type), }; case "array": return { ...example, type: getPythonSdkType(context, example.type), value: example.value.map((x) => serializeTypeExample(context, x)), }; case "dict": return { ...example, type: getPythonSdkType(context, example.type), value: Object.fromEntries( Object.entries(example.value).map(([key, value]) => [ key, serializeTypeExample(context, value), ]), ), }; case "union": return { ...example, type: getPythonSdkType(context, example.type), }; case "model": return { ...example, type: getPythonSdkType(context, example.type), value: Object.fromEntries( Object.entries(example.value).map(([key, value]) => [ key, serializeTypeExample(context, value), ]), ), additionalPropertiesValue: example.additionalPropertiesValue ? Object.fromEntries( Object.entries(example.additionalPropertiesValue).map(([key, value]) => [ key, serializeTypeExample(context, value), ]), ) : undefined, }; }}
Complete Usage Code
lib.ts
import { SdkContext } from "@azure-tools/typespec-client-generator-core";import { PythonSdkType, PythonSdkServiceOperation } from "./interfaces.js";
export interface PythonEmitterOptions { "flavor"?: "azure"; ...}
export interface PythonSdkContext< TServiceOperation extends PythonSdkServiceOperation> extends SdkContext<PythonEmitterOptions, TServiceOperation> { __endpointPathParameters: Record<str, Any>; pythonTypesMap: Record<str, PythonSdkType>;}
emitter.ts
import { EmitContext } from "@typespec/compiler";import { SdkContext, createSdkContext, SdkClientType,} from "@azure-tools/typespec-client-generator-core";import { PythonSdkContext } from "./lib.js";import { getPythonSdkType, getSdkModelPropertyType } from "./types.js";import { PythonSdkType, PythonSdkPackage, PythonSdkClientType, PythonSdkServiceMethod, PythonSdkServiceOperation,} from "./interfaces.js";import { serializeServiceMethod } from "./methods.js";
export async function $onEmit(context: EmitContext<PythonEmitterOptions>) { const sdkContext: PythonSdkContext = { ...createSdkContext(context, "@azure-tools/typespec-python"), __endpointPathParameters: {}, };
const pythonSdkPackage: PythonSdkPackage = { clients: sdkContext.clients.map((x) => serializeClient(sdkContext, x)), rootNamespace: sdkContext.sdkPackage.rootNamespace, models: sdkContext.sdkPackage.models.map((x) => getPythonSdkType(sdkContext, x)), enums: sdkContext.sdkPackage.enums.map((x) => getPythonSdkType(sdkContext, x)), };
linkSubClients(sdkContext);
// Serialize the pythonSdkPackage and pass it to your generator}
function serializeClient<TServiceOperation extends PythonSdkServiceOperation>( sdkContext: PythonSdkContext<TServiceOperation>, client: SdkClientType<TServiceOperation>,): PythonSdkClientType { const pythonClient = { ...client, parameters: client.initialization?.properties.map((x) => getSdkModelPropertyType(sdkContext, x), ), subClients: client.methods .filter((x) => x.kind === "clientaccessor") .map((x) => serializeClient(sdkContext, x.response)), methods: client.methods .filter((x) => x.kind === "method") .map((x) => serializeServiceMethod(sdkContext, x)), subclients: [], }; sdkContext, ClientMap.set(client, pythonClient); return pythonClient;}
function linkSubClients<TServiceOperation extends PythonSdkServiceOperation>( sdkContext: PythonSdkContext<TServiceOperation>,): void { for (const client of sdkContext.clients) { client.subclients = client.methods .filter((x) => x.kind === "clientaccessor") .map((x) => x.returnType) .map((x) => sdkContext.ClientMap.get(x)); }}
methods.ts
import { SdkClientType, SdkServiceMethod, SdkBasicServiceMethod, SdkPagingServiceMethod, SdkLroServiceMethod, SdkLroPagingServiceMethod,} from "@azure-tools/typespec-client-generator-core";import { PythonSdkBasicServiceMethod, PythonSdkPagingServiceMethod, PythonSdkLroServiceMethod, PythonSdkLroPagingServiceMethod, PythonSdkServiceOperation,} from "./interfaces.js";import { PythonSdkContext } from "./lib.js";import { serializeHttpServiceOperation } from "./http.js";
export function serializeServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( sdkContext: PythonSdkContext<TServiceOperation>, method: SdkServiceMethod<TServiceOperation>): PythonSdkServiceMethod<TServiceOperation> { switch (method.kind) { case "basic": return serializeBasicServiceMethod(context, method); case "paging": return serializePagingServiceMethod(context, method); case "lro": return serializeLroServiceMethod(context, method); case "lropaging": return serializeLroPagingServiceMethod(context, method); }}
function serializeBasicServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkBasicServiceMethod<TServiceOperation>,): PythonSdkBasicServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method, method.operation) },}
function serializePagingServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkPagingServiceMethod<TServiceOperation>,): PythonSdkBasicServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method, method.operation), nextLinkOperation: method.nextLinkOperation ? serializeServiceOperation(context, method, method.nextLinkOperation) : undefined, },}
function serializeLroServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkLroServiceMethod<TServiceOperation>,): PythonSdkLroServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method, method.operation), },}
function serializeLroPagingServiceMethod< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkLroPagingServiceMethod<TServiceOperation>,): PythonSdkLroPagingServiceMethod<TServiceOperation> { return { ...method, name: camelToSnakeCase(method.name), operation: serializeServiceOperation(context, method, method.operation), nextLinkOperation: method.nextLinkOperation ? serializeServiceOperation(context, method, method.nextLinkOperation) : undefined, },}
function serializeServiceOperation< TServiceOperation extends PythonSdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, method: SdkServiceMethod<TServiceOperation>, operation: TServiceOperation): TServiceOperation { switch (operation.kind) { case "http": { return serializeHttpServiceOperation( context, operation ) } }}
function serializeHttpServiceOperation( context: PythonSdkContext<SdkHttpOperation>, operation: SdkHttpOperation,): PythonSdkHttpOperation { return { ...operation, parameters: operation.parameters.map( (x) => getSdkModelPropertyType(context, x) ), bodyParam: ( operation.bodyParam ? getSdkModelPropertyType(context, operation.bodyParam) : undefined ), responses: Object.entries(operation.responses).map( ([statusCode, responses]) => serializeHttpResponse(context, method, statusCode, responses) ), exceptions: Object.entries(operation.exceptions).map( ([statusCode, exceptions]) => serializeHttpResponse(context, method, statusCode, exceptions) ), }}
http.js
import { SdkClientType, SdkHttpOperation } from "@azure-tools/typespec-client-generator-core";import { PythonSdkContext } from "./lib.js";import { PythonSdkHttpOperation, PythonSdkHttpResponse } from "./interfaces.js";import { getPythonSdkType, getSdkModelPropertyType } from "./types.js";
function serializeHttpServiceOperation( context: PythonSdkContext<SdkHttpOperation>, method: SdkServiceMethod<SdkHttpOperation>, operation: SdkHttpOperation,): PythonSdkHttpOperation { return { ...operation, parameters: operation.parameters.map((x) => getSdkModelPropertyType(context, x)), bodyParam: operation.bodyParam ? getSdkModelPropertyType(context, operation.bodyParam) : undefined, responses: Object.entries(operation.responses).map(([statusCode, responses]) => serializeHttpResponse(context, method, statusCode, responses), ), exceptions: Object.entries(operation.exceptions).map(([statusCode, exceptions]) => serializeHttpResponse(context, method, statusCode, exceptions), ), };
function serializeHttpResponse( context: PythonSdkContext<SdkHttpOperation>, response: SdkHttpResponse, ): PythonSdkHttpResponse { return { ...response, type: response.type ? getPythonSdkType(context, response.type) : undefined, }; }}
types.ts
import { SdkServiceOperation, SdkType, SdkModelType, SdkModelPropertyType } from "@azure-tools/typespec-client-generator-core";import { PythonSdkContext } from "./lib.js";import { PythonSdkType, PythonSdkModelType, PythonSdkEnumType } from "./interfaces.js";
export function getSdkModelPropertyType< TServiceOperation extends SdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, prop: SdkModelPropertyType): PythonSdkModelType { const type = getPythonSdkType(context, prop.type) switch (prop.kind) { case "query": case "header" case "path": return { ...prop, correspondingMethodParams: prop.correspondingMethodParams.map( (x) => getSdkModelPropertyType(context, x), ) } default: return { ...prop, type, } }}
export function getPythonSdkType< TServiceOperation extends SdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, type: SdkType,): PythonSdkType { // We don't have to convert all types, just types that we want // to add more information onto switch (type.kind) { case "model": return getPythonModelType(context, type); case "enum": return getPythonEnumType(context, type); default: return type; }}
function getPythonModelType< TServiceOperation extends SdkServiceOperation>( context: PythonSdkContext<TServiceOperation>, type: SdkModelType,): PythonSdkModelType { return { ...type, snakeCaseName: camelToSnakeCase(type.name), properties: type.properties.map( (x) => getPythonBodyModelPropertyType( context, x, ) ), baseModel: type.baseModel ? getPythonModelType(context, type.baseModel) : undefined, ... }}