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] make endpoint overridable #1155

Merged
merged 15 commits into from
Aug 6, 2024
Merged
7 changes: 7 additions & 0 deletions .chronus/changes/default_endpoint-2024-6-11-17-47-42.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

Make literal endpoints overridable
42 changes: 32 additions & 10 deletions docs/howtos/DataPlane Generation - DPG/07tcgcTypes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,29 @@ const sdkContext = {
{
kind: "endpoint",
type: {
kind: "endpoint",
serverUrl: "{endpoint}/widget",
templateArguments: [
kind: "union",
values: [
{
name: "endpoint",
kind: "path",
...
kind: "endpoint",
serverUrl: "{endpoint}",
templateArguments: [
{
name: "endpoint",
kind: "path",
...
}
]
},
{
kind: "endpoint",
serverUrl: "{endpoint}/widget",
templateArguments: [
{
name: "endpoint",
kind: "path",
...
}
]
}
]
}
Expand Down Expand Up @@ -973,15 +989,21 @@ These are parameters to client initialization and method son the client. These w

#### SdkEndpointParameter

An `SdkEndpointParameter` represents a parameter to a client's endpoint. If the client is not parameterized, the endpoint parameter represents the entire endpoint input for the client. For parameterized clients, there will be as many `SdkEndpointParameter`s as needed to parameterize the client endpoint.
An `SdkEndpointParameter` represents a parameter to a client's endpoint.

```ts
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.
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
If there are multiple servers, we will return the union of all of the possibilities.

```tsp
export interface SdkEndpointParameter extends SdkModelPropertyTypeBase {
kind: "endpoint";
urlEncode: boolean;
onClient: true;
serializedName?: string;
type: SdkEndpointType;
type: SdkEndpointType | SdkUnionType<SdkEndpointType>;
}
```

Expand Down Expand Up @@ -1407,7 +1429,7 @@ export type AccessFlags = "internal" | "public";
```ts
export interface SdkEndpointType {
kind: "endpoint";
serverUrl?: string;
serverUrl: string;
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
templateArguments: SdkPathParameter[];
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ export interface SdkEndpointParameter extends SdkModelPropertyTypeBase {
urlEncode: boolean;
onClient: true;
serializedName?: string;
type: SdkEndpointType;
type: SdkEndpointType | SdkUnionType;
}

export interface SdkCredentialParameter extends SdkModelPropertyTypeBase {
Expand Down
170 changes: 103 additions & 67 deletions packages/typespec-client-generator-core/src/package.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { getLroMetadata, getPagedResult } from "@azure-tools/typespec-azure-core";
import {
Diagnostic,
Operation,
Type,
createDiagnosticCollector,
Diagnostic,
getNamespaceFullName,
getService,
ignoreDiagnostics,
Operation,
Type,
} from "@typespec/compiler";
import { getServers } from "@typespec/http";
import { getServers, HttpServer } from "@typespec/http";
import { resolveVersions } from "@typespec/versioning";
import { camelCase } from "change-case";
import {
Expand Down Expand Up @@ -44,6 +44,7 @@ import {
SdkServiceOperation,
SdkServiceParameter,
SdkType,
SdkUnionType,
TCGCContext,
UsageFlags,
} from "./interfaces.js";
Expand Down Expand Up @@ -413,78 +414,113 @@ function getSdkMethods<TServiceOperation extends SdkServiceOperation>(
return diagnostics.wrap(retval);
}

function getEndpointTypeFromSingleServer(
context: TCGCContext,
client: SdkClient | SdkOperationGroup,
server: HttpServer | undefined
): [SdkEndpointType[], readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const templateArguments: SdkPathParameter[] = [];
const defaultOverridableEndpointType: SdkEndpointType = {
kind: "endpoint",
serverUrl: "{endpoint}",
templateArguments: [
{
name: "endpoint",
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
isGeneratedName: true,
description: "Service host",
kind: "path",
onClient: true,
urlEncode: false,
optional: false,
iscai-msft marked this conversation as resolved.
Show resolved Hide resolved
serializedName: "endpoint",
correspondingMethodParams: [],
type: getTypeSpecBuiltInType(context, "string"),
isApiVersionParam: false,
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, client.service)}.endpoint`,
decorators: [],
},
],
decorators: [],
};
const types: SdkEndpointType[] = [];
if (!server) return diagnostics.wrap([defaultOverridableEndpointType]);
for (const param of server.parameters.values()) {
const sdkParam = diagnostics.pipe(getSdkHttpParameter(context, param, undefined, "path"));
if (sdkParam.kind === "path") {
templateArguments.push(sdkParam);
sdkParam.onClient = true;
if (param.defaultValue && "value" in param.defaultValue) {
sdkParam.clientDefaultValue = param.defaultValue.value;
}
const apiVersionInfo = updateWithApiVersionInformation(context, param, client.type);
sdkParam.isApiVersionParam = apiVersionInfo.isApiVersionParam;
if (sdkParam.isApiVersionParam) {
sdkParam.clientDefaultValue = apiVersionInfo.clientDefaultValue;
}
sdkParam.apiVersions = getAvailableApiVersions(context, param, client.type);
} else {
diagnostics.add(
createDiagnostic({
code: "server-param-not-path",
target: param,
format: {
templateArgumentName: sdkParam.name,
templateArgumentType: sdkParam.kind,
},
})
);
}
}
const isOverridable =
templateArguments.length === 1 && server.url.startsWith("{") && server.url.endsWith("}");

if (templateArguments.length === 0) {
types.push(defaultOverridableEndpointType);
types[0].templateArguments[0].clientDefaultValue = server.url;
} else {
types.push({
kind: "endpoint",
serverUrl: server.url,
templateArguments,
decorators: [],
});
if (!isOverridable) {
types.push(defaultOverridableEndpointType);
}
}
return diagnostics.wrap(types);
}

function getSdkEndpointParameter(
context: TCGCContext,
client: SdkClient | SdkOperationGroup
): [SdkEndpointParameter, readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();
const servers = getServers(context.program, client.service);
let type: SdkEndpointType;
let optional: boolean = false;
if (servers === undefined || servers.length > 1) {
// if there is no defined server url, or if there are more than one
// we will return a mandatory endpoint parameter in initialization
const name = "endpoint";
type = {
kind: "endpoint",
serverUrl: "{endpoint}",
templateArguments: [
{
name,
isGeneratedName: true,
description: "Service host",
kind: "path",
onClient: true,
urlEncode: false,
optional: false,
serializedName: "endpoint",
correspondingMethodParams: [],
type: getTypeSpecBuiltInType(context, "string"),
isApiVersionParam: false,
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, client.service)}.endpoint`,
decorators: [],
},
],
decorators: [],
};
const types: SdkEndpointType[] = [];

if (servers === undefined) {
// if there is no defined server url, we will return an overridable endpoint
types.push(...diagnostics.pipe(getEndpointTypeFromSingleServer(context, client, undefined)));
} else {
// this means we have one server
const templateArguments: SdkPathParameter[] = [];
for (const server of servers) {
types.push(...diagnostics.pipe(getEndpointTypeFromSingleServer(context, client, server)));
}
}
let type: SdkEndpointType | SdkUnionType;
if (types.length > 1) {
type = {
kind: "endpoint",
serverUrl: servers[0].url,
templateArguments,
kind: "union",
values: types,
name: createGeneratedName(context, client.service, "Endpoint"),
isGeneratedName: true,
crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, client.service),
decorators: [],
};
for (const param of servers[0].parameters.values()) {
const sdkParam = diagnostics.pipe(getSdkHttpParameter(context, param, undefined, "path"));
if (sdkParam.kind === "path") {
templateArguments.push(sdkParam);
sdkParam.onClient = true;
if (param.defaultValue && "value" in param.defaultValue) {
sdkParam.clientDefaultValue = param.defaultValue.value;
}
const apiVersionInfo = updateWithApiVersionInformation(context, param, client.type);
sdkParam.isApiVersionParam = apiVersionInfo.isApiVersionParam;
if (sdkParam.isApiVersionParam) {
sdkParam.clientDefaultValue = apiVersionInfo.clientDefaultValue;
}
sdkParam.apiVersions = getAvailableApiVersions(context, param, client.type);
} else {
diagnostics.add(
createDiagnostic({
code: "server-param-not-path",
target: param,
format: {
templateArgumentName: sdkParam.name,
templateArgumentType: sdkParam.kind,
},
})
);
}
}
optional = Boolean(servers[0].url.length && templateArguments.every((param) => param.optional));
} else {
type = types[0];
}
return diagnostics.wrap({
kind: "endpoint",
Expand All @@ -495,7 +531,7 @@ function getSdkEndpointParameter(
onClient: true,
urlEncode: false,
apiVersions: context.__tspTypeToApiVersions.get(client.type)!,
optional,
optional: false,
isApiVersionParam: false,
crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, client.service)}.endpoint`,
decorators: [],
Expand Down
Loading
Loading