Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-client-generator-core"
---

Support nested service namespaces in per-service `api-version` configuration.
2 changes: 1 addition & 1 deletion packages/typespec-client-generator-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ When set to `true`, the emitter will generate convenience methods for each servi

**Type:** `string | object`

Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace's full name to its desired version; services not listed default to their latest version.
Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace to its desired version. Nested namespaces must be represented as nested objects in `tspconfig.yaml`; services not listed default to their latest version.

**Options:**

Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-client-generator-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export async function createSdkContext<
generateProtocolMethods: generateProtocolMethods,
generateConvenienceMethods: generateConvenienceMethods,
namespaceFlag: context.options["namespace"],
apiVersion: context.options["api-version"],
apiVersion: context.options["api-version"] as TCGCContext["apiVersion"],
license: context.options["license"],
decoratorsAllowList: [...defaultDecoratorsAllowList, ...(options?.additionalDecorators ?? [])],
previewStringRegex: options?.versioning?.previewStringRegex || tcgcContext.previewStringRegex,
Expand Down
6 changes: 6 additions & 0 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ import type { ContextNode } from "./internal-utils.js";

type SourceKind = "RequestParameter" | "RequestBody" | "ResponseBody";

export type ApiVersionConfig = string | ApiVersionServiceMap;

export interface ApiVersionServiceMap {
[namespaceSegment: string]: string | ApiVersionServiceMap;
}

export interface TCGCContext {
program: Program;
diagnostics: readonly Diagnostic[];
Expand Down
35 changes: 29 additions & 6 deletions packages/typespec-client-generator-core/src/internal-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import {
getParamAlias,
} from "./decorators.js";
import type {
ApiVersionConfig,
ApiVersionServiceMap,
DecoratorInfo,
ExternalTypeInfo,
SdkBuiltInType,
Expand Down Expand Up @@ -98,7 +100,7 @@ export interface TCGCEmitterOptions extends BrandedSdkEmitterOptionsInterface {
export interface UnbrandedSdkEmitterOptionsInterface {
"generate-protocol-methods"?: boolean;
"generate-convenience-methods"?: boolean;
"api-version"?: string | Record<string, string>;
"api-version"?: ApiVersionConfig;
license?: {
name: string;
company?: string;
Expand Down Expand Up @@ -1016,9 +1018,10 @@ export function handleVersioningMutationForGlobalNamespace(context: TCGCContext)
* - When the option is a string: `latest` is a global keyword; any other string
* (a specific version or `all`) applies only to the single service case and is
* ignored for multi-service packages.
* - When the option is a record, the version is looked up by the service
* namespace's full name. Services that are not listed return `undefined`
* (meaning "use the latest version").
* - When the option is a map, the version is looked up first by the service
* namespace's full name and then by traversing nested namespace segments.
* Services that are not listed return `undefined` (meaning "use the latest
* version").
*
* Multi-service packages do not support the special `all` value (in either the
* string or the record form); it is ignored and treated as `undefined` (use the
Expand All @@ -1042,14 +1045,34 @@ export function resolveApiVersionForService(
// multi-service packages do not support `all`.
return isMultiService ? undefined : config;
}
// Record case: map each service namespace's full name to a version.
// Map case: map each service namespace's full name or nested segments to a version.
if (serviceNamespace === undefined) return undefined;
const version = config[getNamespaceFullName(serviceNamespace)];
const version = resolveApiVersionFromServiceMap(
config as ApiVersionServiceMap,
getNamespaceFullName(serviceNamespace),
);
// Multi-service packages do not support `all`; fall back to the latest version.
if (version === "all" && isMultiService) return undefined;
return version;
}

function resolveApiVersionFromServiceMap(
config: ApiVersionServiceMap,
namespaceName: string,
): string | undefined {
const flatVersion = config[namespaceName];
if (typeof flatVersion === "string") return flatVersion;

let current: string | ApiVersionServiceMap | undefined = config;
for (const segment of namespaceName.split(".")) {
if (typeof current !== "object" || current === null || Array.isArray(current)) {
return undefined;
}
current = current[segment];
}
return typeof current === "string" ? current : undefined;
}

/**
* Find the service namespace that owns the given type. Starts from the type's
* versioned namespace and walks up the enclosing namespaces until it reaches a
Expand Down
15 changes: 12 additions & 3 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
} from "./internal-utils.js";

// `api-version` accepts either a string (single service / `latest` / `all`) or a
// map from service namespace full name to version (multi-service).
// map from service namespace names or nested namespace segments to versions.
const apiVersionSchema = {
oneOf: [
{
Expand All @@ -20,13 +20,22 @@ const apiVersionSchema = {
},
{
type: "object",
additionalProperties: { type: "string" },
additionalProperties: {
oneOf: [
{ type: "string" },
{
type: "object",
additionalProperties: true,
required: [],
},
],
},
required: [],
nullable: true,
},
],
description:
"Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace's full name to its desired version; services not listed default to their latest version.",
"Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace to its desired version. Nested namespaces must be represented as nested objects in `tspconfig.yaml`; services not listed default to their latest version.",
} as any;

export const UnbrandedSdkEmitterOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,101 @@ it("multiple services without versioning", async () => {
strictEqual(biMethod.operation.examples[0].name, "Test operation from ServiceB");
});

it("multiple nested services use configured api versions to find examples", async () => {
const instance = await SimpleBaseTester.createInstance();
instance.fs.addTypeSpecFile(
"./Network/examples/v1/NetworkOperations_get.json",
JSON.stringify({
operationId: "NetworkOperations_get",
title: "Network v1",
responses: { "200": { body: "network-v1" } },
}),
);
instance.fs.addTypeSpecFile(
"./Network/examples/v2/NetworkOperations_get.json",
JSON.stringify({
operationId: "NetworkOperations_get",
title: "Network v2",
responses: { "200": { body: "network-v2" } },
}),
);
instance.fs.addTypeSpecFile(
"./Compute/examples/v1/ComputeOperations_get.json",
JSON.stringify({
operationId: "ComputeOperations_get",
title: "Compute v1",
responses: { "200": { body: "compute-v1" } },
}),
);
instance.fs.addTypeSpecFile(
"./Compute/examples/v2/ComputeOperations_get.json",
JSON.stringify({
operationId: "ComputeOperations_get",
title: "Compute v2",
responses: { "200": { body: "compute-v2" } },
}),
);

const { program } = await instance.compile(
createClientCustomizationInput(
`
@service
@versioned(Microsoft.Network.Versions)
namespace Microsoft.Network {
enum Versions {
v1,
v2,
}
interface NetworkOperations {
op get(): string;
}
}

@service
@versioned(Microsoft.Compute.Versions)
namespace Microsoft.Compute {
enum Versions {
v1,
v2,
}
interface ComputeOperations {
op get(): string;
}
}
`,
`
@client({
name: "CombinedClient",
service: [Microsoft.Network, Microsoft.Compute],
autoMergeService: true,
})
namespace Combined;
`,
),
);
const context = await createSdkContextForTester(program, {
"api-version": {
Microsoft: {
Network: "v1",
Compute: "v1",
},
},
});

const client = context.sdkPackage.clients[0];
const networkClient = client.children?.find((child) => child.name === "NetworkOperations");
ok(networkClient);
const networkMethod = networkClient.methods[0] as SdkServiceMethod<SdkHttpOperation>;
strictEqual(networkMethod.operation.examples?.[0].filePath, "v1/NetworkOperations_get.json");
strictEqual(networkMethod.operation.examples?.[0].name, "Network v1");

const computeClient = client.children?.find((child) => child.name === "ComputeOperations");
ok(computeClient);
const computeMethod = computeClient.methods[0] as SdkServiceMethod<SdkHttpOperation>;
strictEqual(computeMethod.operation.examples?.[0].filePath, "v1/ComputeOperations_get.json");
strictEqual(computeMethod.operation.examples?.[0].name, "Compute v1");
});

it("multiple services without examples", async () => {
const { program } = await SimpleBaseTester.compile(
createClientCustomizationInput(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { getDirectoryPath, resolveCompilerOptions } from "@typespec/compiler";
import { resolveVirtualPath } from "@typespec/compiler/testing";
import { deepStrictEqual, ok, strictEqual } from "assert";
import { it } from "vitest";
import { parse } from "yaml";
import {
createClientCustomizationInput,
createSdkContextForTester,
Expand Down Expand Up @@ -36,6 +39,84 @@ it("single service with versioning should populate apiVersions map", async () =>
strictEqual(sdkPackage.metadata.apiVersions.get("WidgetService"), "v3");
});

it("supports nested service namespaces in tspconfig.yaml", async () => {
const tester = await SimpleBaseTester.createInstance();
const spec = createClientCustomizationInput(
`
@service
@versioned(Microsoft.Network.Versions)
namespace Microsoft.Network {
enum Versions {
v1,
v2,
}
op networkTest(): void;
}

@service
@versioned(Microsoft.Compute.Versions)
namespace Microsoft.Compute {
enum Versions {
v1,
v2,
}
op computeTest(): void;
}
`,
`
@client({
name: "CombinedClient",
service: [Microsoft.Network, Microsoft.Compute],
autoMergeService: true,
})
namespace Combined;
`,
);
tester.fs.addTypeSpecFile("main.tsp", "");
tester.fs.addTypeSpecFile(
"tspconfig.yaml",
`
emit:
- "@azure-tools/typespec-client-generator-core"
options:
"@azure-tools/typespec-client-generator-core":
api-version:
Microsoft:
Network: v1
Compute: v1
`,
);

const entrypoint = resolveVirtualPath("main.tsp");
const [compilerOptions, configDiagnostics] = await resolveCompilerOptions(
tester.fs.compilerHost,
{
cwd: getDirectoryPath(entrypoint),
entrypoint,
},
);
strictEqual(configDiagnostics.length, 0);

const [, diagnostics] = await tester.compileAndDiagnose(spec, {
compilerOptions,
});
strictEqual(
diagnostics.length,
0,
diagnostics.map((diagnostic) => diagnostic.message).join("\n"),
);

const output = [...tester.fs.fs.entries()].find(([path]) =>
path.endsWith("tcgc-output.yaml"),
)?.[1];
ok(output);
const sdkPackage = parse(output);
deepStrictEqual(sdkPackage.metadata.apiVersions, {
"Microsoft.Network": "v1",
"Microsoft.Compute": "v1",
});
});

it("multiple services should populate apiVersions map with all services", async () => {
const { program } = await SimpleBaseTester.compile(
createClientCustomizationInput(
Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ When set to `true`, the generated SDK uses `getter` method to access child clien

**Type:** `string | object`

Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace's full name to its desired version; services not listed default to their latest version.
Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace to its desired version. Nested namespaces must be represented as nested objects in `tspconfig.yaml`; services not listed default to their latest version.

**Options:**

Expand Down
2 changes: 1 addition & 1 deletion packages/typespec-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Whether to generate test files, for basic testing of your generated sdks. Defaul

**Type:** `string | object`

Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace's full name to its desired version; services not listed default to their latest version.
Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace to its desired version. Nested namespaces must be represented as nested objects in `tspconfig.yaml`; services not listed default to their latest version.

**Options:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ When set to `true`, the generated SDK uses `getter` method to access child clien

**Type:** `string | object`

Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace's full name to its desired version; services not listed default to their latest version.
Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace to its desired version. Nested namespaces must be represented as nested objects in `tspconfig.yaml`; services not listed default to their latest version.

**Options:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Whether to generate test files, for basic testing of your generated sdks. Defaul

**Type:** `string | object`

Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace's full name to its desired version; services not listed default to their latest version.
Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace to its desired version. Nested namespaces must be represented as nested objects in `tspconfig.yaml`; services not listed default to their latest version.

**Options:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ When set to `true`, the emitter will generate convenience methods for each servi

**Type:** `string | object`

Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace's full name to its desired version; services not listed default to their latest version.
Use this flag if you would like to generate the sdk only for a specific version. Default value is the latest version. Also accepts values `latest` and `all`. For multi-service packages, provide a map from each service namespace to its desired version. Nested namespaces must be represented as nested objects in `tspconfig.yaml`; services not listed default to their latest version.

**Options:**

Expand Down
Loading