diff --git a/.changeset/stale-phones-behave.md b/.changeset/stale-phones-behave.md new file mode 100644 index 00000000000..899d8f7e086 --- /dev/null +++ b/.changeset/stale-phones-behave.md @@ -0,0 +1,5 @@ +--- +"@smithy/middleware-endpoint": minor +--- + +handle clientContextParam collisions with builtin config keys diff --git a/packages/eventstream-serde-universal/src/clientContextParams-precedence.integ.spec.ts b/packages/eventstream-serde-universal/src/clientContextParams-precedence.integ.spec.ts new file mode 100644 index 00000000000..3664bfac5f9 --- /dev/null +++ b/packages/eventstream-serde-universal/src/clientContextParams-precedence.integ.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, test as it } from "vitest"; +import { XYZService } from "xyz"; + +describe("client context parameters precedence integration test", () => { + it("should handle conflicting vs non-conflicting parameter precedence correctly", async () => { + // For non-conflicting params + const clientWithNonConflicting = new XYZService({ + endpoint: "https://localhost", + apiKey: async () => ({ apiKey: "test-key" }), + customParam: "user-custom-value", + clientContextParams: { + apiKey: "test-key", + customParam: "nested-custom-value", + }, + }); + + // Verify that endpoint resolution uses the nested value over root value + const resolvedConfig = clientWithNonConflicting.config; + const effectiveCustomParam = resolvedConfig.clientContextParams?.customParam ?? resolvedConfig.customParam; + expect(effectiveCustomParam).toBe("nested-custom-value"); + + // For conflicting parameters + const clientWithConflicting = new XYZService({ + endpoint: "https://localhost", + apiKey: async () => ({ apiKey: "auth-key" }), + clientContextParams: { + apiKey: "endpoint-key", + }, + }); + + // Verify that both auth and endpoint contexts can coexist + const resolvedConfigConflicting = clientWithConflicting.config; + + // Verify endpoint context has the nested value + expect(resolvedConfigConflicting.clientContextParams?.apiKey).toBe("endpoint-key"); + + // Verify auth context has the auth provider + const authIdentity = await resolvedConfigConflicting.apiKey?.(); + expect(authIdentity?.apiKey).toBe("auth-key"); + }); +}); diff --git a/packages/eventstream-serde-universal/src/eventstream-cbor.integ.spec.ts b/packages/eventstream-serde-universal/src/eventstream-cbor.integ.spec.ts index f509228a692..c2c937886b9 100644 --- a/packages/eventstream-serde-universal/src/eventstream-cbor.integ.spec.ts +++ b/packages/eventstream-serde-universal/src/eventstream-cbor.integ.spec.ts @@ -9,6 +9,10 @@ describe("local model integration test for cbor eventstreams", () => { it("should read and write cbor event streams", async () => { const client = new XYZService({ endpoint: "https://localhost", + apiKey: async () => ({ apiKey: "test-api-key" }), + clientContextParams: { + apiKey: "test-api-key", + }, }); const body = cbor.serialize({ diff --git a/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.spec.ts b/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.spec.ts index 1c7e6a06bd1..296a7f69cab 100644 --- a/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.spec.ts +++ b/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.spec.ts @@ -58,4 +58,31 @@ describe(createConfigValueProvider.name, () => { expect(await createConfigValueProvider("v1", "endpoint", config)()).toEqual(sampleUrl); expect(await createConfigValueProvider("v2", "endpoint", config)()).toEqual(sampleUrl); }); + + it("should prioritize clientContextParams over direct properties", async () => { + const config = { + stage: "prod", + clientContextParams: { + stage: "beta", + }, + }; + expect(await createConfigValueProvider("stage", "stage", config, true)()).toEqual("beta"); + }); + + it("should fall back to direct property when clientContextParams is not provided", async () => { + const config = { + customParam: "direct-value", + }; + expect(await createConfigValueProvider("customParam", "customParam", config)()).toEqual("direct-value"); + }); + + it("should fall back to direct property when clientContextParams exists but param is not in it", async () => { + const config = { + customParam: "direct-value", + clientContextParams: { + otherParam: "other-value", + }, + }; + expect(await createConfigValueProvider("customParam", "customParam", config)()).toEqual("direct-value"); + }); }); diff --git a/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.ts b/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.ts index b6197422d6f..38f87ff6a8f 100644 --- a/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.ts +++ b/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.ts @@ -9,16 +9,29 @@ import type { Endpoint, EndpointV2 } from "@smithy/types"; * it will most likely not contain the config * value, but we use it as a fallback. * @param config - container of the config values. + * @param isClientContextParam - whether this is a client context parameter. * * @returns async function that will resolve with the value. */ export const createConfigValueProvider = >( configKey: string, canonicalEndpointParamKey: string, - config: Config + config: Config, + isClientContextParam = false ) => { const configProvider = async () => { - const configValue: unknown = config[configKey] ?? config[canonicalEndpointParamKey]; + let configValue: unknown; + + if (isClientContextParam) { + // For client context parameters, check clientContextParams first + const clientContextParams = config.clientContextParams as Record | undefined; + const nestedValue: unknown = clientContextParams?.[canonicalEndpointParamKey]; + configValue = nestedValue ?? config[configKey] ?? config[canonicalEndpointParamKey]; + } else { + // For built-in parameters and other config properties + configValue = config[configKey] ?? config[canonicalEndpointParamKey]; + } + if (typeof configValue === "function") { return configValue(); } diff --git a/packages/middleware-endpoint/src/adaptors/getEndpointFromInstructions.ts b/packages/middleware-endpoint/src/adaptors/getEndpointFromInstructions.ts index 4c3be0f276b..2f57f70c2bd 100644 --- a/packages/middleware-endpoint/src/adaptors/getEndpointFromInstructions.ts +++ b/packages/middleware-endpoint/src/adaptors/getEndpointFromInstructions.ts @@ -91,7 +91,12 @@ export const resolveParams = async < break; case "clientContextParams": case "builtInParams": - endpointParams[name] = await createConfigValueProvider(instruction.name, name, clientConfig)(); + endpointParams[name] = await createConfigValueProvider( + instruction.name, + name, + clientConfig, + instruction.type !== "builtInParams" + )(); break; case "operationContextParams": endpointParams[name] = instruction.get(commandInput); diff --git a/packages/util-retry/src/retries.integ.spec.ts b/packages/util-retry/src/retries.integ.spec.ts index f5d37cef458..6dbe544ca0c 100644 --- a/packages/util-retry/src/retries.integ.spec.ts +++ b/packages/util-retry/src/retries.integ.spec.ts @@ -20,6 +20,7 @@ describe("retries", () => { it("should retry throttling and transient-error status codes", async () => { const client = new XYZService({ endpoint: "https://localhost/nowhere", + apiKey: { apiKey: "test-api-key" }, }); requireRequestsFrom(client) @@ -50,6 +51,7 @@ describe("retries", () => { it("should retry when a retryable trait is modeled", async () => { const client = new XYZService({ endpoint: "https://localhost/nowhere", + apiKey: { apiKey: "test-api-key" }, }); requireRequestsFrom(client) @@ -80,6 +82,7 @@ describe("retries", () => { it("should retry retryable trait with throttling", async () => { const client = new XYZService({ endpoint: "https://localhost/nowhere", + apiKey: { apiKey: "test-api-key" }, }); requireRequestsFrom(client) @@ -110,6 +113,7 @@ describe("retries", () => { it("should not retry if the error is not modeled with retryable trait and is not otherwise retryable", async () => { const client = new XYZService({ endpoint: "https://localhost/nowhere", + apiKey: { apiKey: "test-api-key" }, }); requireRequestsFrom(client) diff --git a/private/my-local-model-schema/src/XYZServiceClient.ts b/private/my-local-model-schema/src/XYZServiceClient.ts index 8e0a2ddfd38..dc194989140 100644 --- a/private/my-local-model-schema/src/XYZServiceClient.ts +++ b/private/my-local-model-schema/src/XYZServiceClient.ts @@ -13,11 +13,8 @@ import { import { getContentLengthPlugin } from "@smithy/middleware-content-length"; import { type EndpointInputConfig, - type EndpointRequiredInputConfig, - type EndpointRequiredResolvedConfig, type EndpointResolvedConfig, resolveEndpointConfig, - resolveEndpointRequiredConfig, } from "@smithy/middleware-endpoint"; import { type RetryInputConfig, @@ -203,7 +200,6 @@ export type XYZServiceClientConfigType = Partial<__SmithyConfiguration<__HttpHan ClientDefaults & RetryInputConfig & EndpointInputConfig & - EndpointRequiredInputConfig & EventStreamSerdeInputConfig & HttpAuthSchemeInputConfig & ClientInputEndpointParameters; @@ -222,7 +218,6 @@ export type XYZServiceClientResolvedConfigType = __SmithyResolvedConfiguration<_ RuntimeExtensionsConfig & RetryResolvedConfig & EndpointResolvedConfig & - EndpointRequiredResolvedConfig & EventStreamSerdeResolvedConfig & HttpAuthSchemeResolvedConfig & ClientResolvedEndpointParameters; @@ -255,11 +250,10 @@ export class XYZServiceClient extends __Client< const _config_1 = resolveClientEndpointParameters(_config_0); const _config_2 = resolveRetryConfig(_config_1); const _config_3 = resolveEndpointConfig(_config_2); - const _config_4 = resolveEndpointRequiredConfig(_config_3); - const _config_5 = resolveEventStreamSerdeConfig(_config_4); - const _config_6 = resolveHttpAuthSchemeConfig(_config_5); - const _config_7 = resolveRuntimeExtensions(_config_6, configuration?.extensions || []); - this.config = _config_7; + const _config_4 = resolveEventStreamSerdeConfig(_config_3); + const _config_5 = resolveHttpAuthSchemeConfig(_config_4); + const _config_6 = resolveRuntimeExtensions(_config_5, configuration?.extensions || []); + this.config = _config_6; this.middlewareStack.use(getSchemaSerdePlugin(this.config)); this.middlewareStack.use(getRetryPlugin(this.config)); this.middlewareStack.use(getContentLengthPlugin(this.config)); @@ -267,7 +261,8 @@ export class XYZServiceClient extends __Client< getHttpAuthSchemeEndpointRuleSetPlugin(this.config, { httpAuthSchemeParametersProvider: defaultXYZServiceHttpAuthSchemeParametersProvider, identityProviderConfigProvider: async (config: XYZServiceClientResolvedConfig) => - new DefaultIdentityProviderConfig({}), + new DefaultIdentityProviderConfig({ + "smithy.api#httpApiKeyAuth": config.apiKey,}), }) ); this.middlewareStack.use(getHttpSigningPlugin(this.config)); diff --git a/private/my-local-model-schema/src/auth/httpAuthExtensionConfiguration.ts b/private/my-local-model-schema/src/auth/httpAuthExtensionConfiguration.ts index dada1d85ed3..c0d91baa32f 100644 --- a/private/my-local-model-schema/src/auth/httpAuthExtensionConfiguration.ts +++ b/private/my-local-model-schema/src/auth/httpAuthExtensionConfiguration.ts @@ -1,5 +1,5 @@ // smithy-typescript generated code -import type { HttpAuthScheme } from "@smithy/types"; +import type { ApiKeyIdentity, ApiKeyIdentityProvider, HttpAuthScheme } from "@smithy/types"; import type { XYZServiceHttpAuthSchemeProvider } from "./httpAuthSchemeProvider"; @@ -11,6 +11,8 @@ export interface HttpAuthExtensionConfiguration { httpAuthSchemes(): HttpAuthScheme[]; setHttpAuthSchemeProvider(httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider): void; httpAuthSchemeProvider(): XYZServiceHttpAuthSchemeProvider; + setApiKey(apiKey: ApiKeyIdentity | ApiKeyIdentityProvider): void; + apiKey(): ApiKeyIdentity | ApiKeyIdentityProvider | undefined; } /** @@ -19,6 +21,7 @@ export interface HttpAuthExtensionConfiguration { export type HttpAuthRuntimeConfig = Partial<{ httpAuthSchemes: HttpAuthScheme[]; httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider; + apiKey: ApiKeyIdentity | ApiKeyIdentityProvider; }>; /** @@ -29,6 +32,7 @@ export const getHttpAuthExtensionConfiguration = ( ): HttpAuthExtensionConfiguration => { const _httpAuthSchemes = runtimeConfig.httpAuthSchemes!; let _httpAuthSchemeProvider = runtimeConfig.httpAuthSchemeProvider!; + let _apiKey = runtimeConfig.apiKey; return { setHttpAuthScheme(httpAuthScheme: HttpAuthScheme): void { const index = _httpAuthSchemes.findIndex((scheme) => scheme.schemeId === httpAuthScheme.schemeId); @@ -47,6 +51,12 @@ export const getHttpAuthExtensionConfiguration = ( httpAuthSchemeProvider(): XYZServiceHttpAuthSchemeProvider { return _httpAuthSchemeProvider; }, + setApiKey(apiKey: ApiKeyIdentity | ApiKeyIdentityProvider): void { + _apiKey = apiKey; + }, + apiKey(): ApiKeyIdentity | ApiKeyIdentityProvider | undefined { + return _apiKey; + }, }; }; @@ -57,5 +67,6 @@ export const resolveHttpAuthRuntimeConfig = (config: HttpAuthExtensionConfigurat return { httpAuthSchemes: config.httpAuthSchemes(), httpAuthSchemeProvider: config.httpAuthSchemeProvider(), + apiKey: config.apiKey(), }; }; diff --git a/private/my-local-model-schema/src/auth/httpAuthSchemeProvider.ts b/private/my-local-model-schema/src/auth/httpAuthSchemeProvider.ts index ff178de097d..c3f6b846ca4 100644 --- a/private/my-local-model-schema/src/auth/httpAuthSchemeProvider.ts +++ b/private/my-local-model-schema/src/auth/httpAuthSchemeProvider.ts @@ -1,12 +1,16 @@ // smithy-typescript generated code -import type { - HandlerExecutionContext, - HttpAuthOption, - HttpAuthScheme, - HttpAuthSchemeParameters, - HttpAuthSchemeParametersProvider, - HttpAuthSchemeProvider, - Provider, +import { doesIdentityRequireRefresh, isIdentityExpired, memoizeIdentityProvider } from "@smithy/core"; +import { + type ApiKeyIdentity, + type ApiKeyIdentityProvider, + type HandlerExecutionContext, + type HttpAuthOption, + type HttpAuthScheme, + type HttpAuthSchemeParameters, + type HttpAuthSchemeParametersProvider, + type HttpAuthSchemeProvider, + type Provider, + HttpApiKeyAuthLocation, } from "@smithy/types"; import { getSmithyContext, normalizeProvider } from "@smithy/util-middleware"; @@ -41,9 +45,14 @@ export const defaultXYZServiceHttpAuthSchemeParametersProvider = async ( }; }; -function createSmithyApiNoAuthHttpAuthOption(authParameters: XYZServiceHttpAuthSchemeParameters): HttpAuthOption { +function createSmithyApiHttpApiKeyAuthHttpAuthOption(authParameters: XYZServiceHttpAuthSchemeParameters): HttpAuthOption { return { - schemeId: "smithy.api#noAuth", + schemeId: "smithy.api#httpApiKeyAuth", + signingProperties: { + name: "X-Api-Key", + in: HttpApiKeyAuthLocation.HEADER, + scheme: undefined, + }, }; } @@ -59,7 +68,7 @@ export const defaultXYZServiceHttpAuthSchemeProvider: XYZServiceHttpAuthSchemePr const options: HttpAuthOption[] = []; switch (authParameters.operation) { default: { - options.push(createSmithyApiNoAuthHttpAuthOption(authParameters)); + options.push(createSmithyApiHttpApiKeyAuthHttpAuthOption(authParameters)); } } return options; @@ -88,6 +97,10 @@ export interface HttpAuthSchemeInputConfig { * @internal */ httpAuthSchemeProvider?: XYZServiceHttpAuthSchemeProvider; + /** + * The API key to use when making requests. + */ + apiKey?: ApiKeyIdentity | ApiKeyIdentityProvider; } /** @@ -113,6 +126,10 @@ export interface HttpAuthSchemeResolvedConfig { * @internal */ readonly httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider; + /** + * The API key to use when making requests. + */ + readonly apiKey?: ApiKeyIdentityProvider; } /** @@ -121,7 +138,9 @@ export interface HttpAuthSchemeResolvedConfig { export const resolveHttpAuthSchemeConfig = ( config: T & HttpAuthSchemeInputConfig ): T & HttpAuthSchemeResolvedConfig => { + const apiKey = memoizeIdentityProvider(config.apiKey, isIdentityExpired, doesIdentityRequireRefresh); return Object.assign(config, { authSchemePreference: normalizeProvider(config.authSchemePreference ?? []), + apiKey, }) as T & HttpAuthSchemeResolvedConfig; }; diff --git a/private/my-local-model-schema/src/endpoint/EndpointParameters.ts b/private/my-local-model-schema/src/endpoint/EndpointParameters.ts index 4bd1abf15e5..3dc9eafeb29 100644 --- a/private/my-local-model-schema/src/endpoint/EndpointParameters.ts +++ b/private/my-local-model-schema/src/endpoint/EndpointParameters.ts @@ -5,7 +5,20 @@ import type { Endpoint, EndpointParameters as __EndpointParameters, EndpointV2, * @public */ export interface ClientInputEndpointParameters { + clientContextParams?: { + apiKey?: string | undefined | Provider; + region?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; + logger?: string | undefined | Provider; + }; endpoint?: string | Provider | Endpoint | Provider | EndpointV2 | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; } /** @@ -15,6 +28,13 @@ export type ClientResolvedEndpointParameters = Omit( options: T & ClientInputEndpointParameters ): T & ClientResolvedEndpointParameters => { return Object.assign(options, { + customParam: options.customParam ?? "default-custom-value", + enableFeature: options.enableFeature ?? true, + debugMode: options.debugMode ?? false, + nonConflictingParam: options.nonConflictingParam ?? "non-conflict-default", defaultSigningName: "", + clientContextParams: Object.assign(clientContextParamDefaults, options.clientContextParams), }); }; @@ -30,6 +55,13 @@ export const resolveClientEndpointParameters = ( * @internal */ export const commonParams = { + ApiKey: { type: "clientContextParams", name: "apiKey" }, + nonConflictingParam: { type: "clientContextParams", name: "nonConflictingParam" }, + logger: { type: "clientContextParams", name: "logger" }, + region: { type: "clientContextParams", name: "region" }, + customParam: { type: "clientContextParams", name: "customParam" }, + debugMode: { type: "clientContextParams", name: "debugMode" }, + enableFeature: { type: "clientContextParams", name: "enableFeature" }, endpoint: { type: "builtInParams", name: "endpoint" }, } as const; @@ -37,5 +69,12 @@ export const commonParams = { * @internal */ export interface EndpointParameters extends __EndpointParameters { - endpoint?: string | undefined; + endpoint: string; + ApiKey?: string | undefined; + region?: string | undefined; + customParam?: string | undefined; + enableFeature?: boolean | undefined; + debugMode?: boolean | undefined; + nonConflictingParam?: string | undefined; + logger?: string | undefined; } diff --git a/private/my-local-model-schema/src/endpoint/endpointResolver.ts b/private/my-local-model-schema/src/endpoint/endpointResolver.ts index aaeb8e220ba..174cbdace53 100644 --- a/private/my-local-model-schema/src/endpoint/endpointResolver.ts +++ b/private/my-local-model-schema/src/endpoint/endpointResolver.ts @@ -7,7 +7,8 @@ import { ruleSet } from "./ruleset"; const cache = new EndpointCache({ size: 50, - params: ["endpoint"], + params: ["ApiKey", + "endpoint"], }); /** diff --git a/private/my-local-model-schema/src/endpoint/ruleset.ts b/private/my-local-model-schema/src/endpoint/ruleset.ts index d8b2ce3d908..99179427a9c 100644 --- a/private/my-local-model-schema/src/endpoint/ruleset.ts +++ b/private/my-local-model-schema/src/endpoint/ruleset.ts @@ -5,9 +5,50 @@ export const ruleSet: RuleSetObject = { version: "1.0", parameters: { endpoint: { - type: "string", builtIn: "SDK::Endpoint", - documentation: "Endpoint used for making requests. Should be formatted as a URI.", + required: true, + documentation: "The endpoint used to send the request.", + type: "string", + }, + ApiKey: { + required: false, + documentation: "ApiKey", + type: "string", + }, + region: { + type: "string", + required: false, + documentation: "AWS region", + }, + customParam: { + type: "string", + required: true, + default: "default-custom-value", + documentation: "Custom parameter for testing", + }, + enableFeature: { + type: "boolean", + required: true, + default: true, + documentation: "Feature toggle with default", + }, + debugMode: { + type: "boolean", + required: true, + default: false, + documentation: "Debug mode with default", + }, + nonConflictingParam: { + type: "string", + required: true, + default: "non-conflict-default", + documentation: "Non-conflicting with default", + }, + logger: { + type: "string", + required: true, + default: "default-logger", + documentation: "Conflicting logger with default", }, }, rules: [ @@ -17,22 +58,30 @@ export const ruleSet: RuleSetObject = { fn: "isSet", argv: [ { - ref: "endpoint", + ref: "ApiKey", }, ], }, ], endpoint: { - url: { - ref: "endpoint", + url: "{endpoint}", + properties: {}, + headers: { + "x-api-key": [ + "{ApiKey}", + ], }, }, type: "endpoint", }, { conditions: [], - error: "(default endpointRuleSet) endpoint is not set - you must configure an endpoint.", - type: "error", + endpoint: { + url: "{endpoint}", + properties: {}, + headers: {}, + }, + type: "endpoint", }, ], }; diff --git a/private/my-local-model-schema/src/runtimeConfig.shared.ts b/private/my-local-model-schema/src/runtimeConfig.shared.ts index 636f6d578ab..e840ee49657 100644 --- a/private/my-local-model-schema/src/runtimeConfig.shared.ts +++ b/private/my-local-model-schema/src/runtimeConfig.shared.ts @@ -1,5 +1,5 @@ // smithy-typescript generated code -import { NoAuthSigner } from "@smithy/core"; +import { HttpApiKeyAuthSigner } from "@smithy/core"; import { SmithyRpcV2CborProtocol } from "@smithy/core/cbor"; import { NoOpLogger } from "@smithy/smithy-client"; import type { IdentityProviderConfig } from "@smithy/types"; @@ -25,10 +25,10 @@ export const getRuntimeConfig = (config: XYZServiceClientConfig) => { httpAuthSchemeProvider: config?.httpAuthSchemeProvider ?? defaultXYZServiceHttpAuthSchemeProvider, httpAuthSchemes: config?.httpAuthSchemes ?? [ { - schemeId: "smithy.api#noAuth", + schemeId: "smithy.api#httpApiKeyAuth", identityProvider: (ipc: IdentityProviderConfig) => - ipc.getIdentityProvider("smithy.api#noAuth") || (async () => ({})), - signer: new NoAuthSigner(), + ipc.getIdentityProvider("smithy.api#httpApiKeyAuth"), + signer: new HttpApiKeyAuthSigner(), }, ], logger: config?.logger ?? new NoOpLogger(), diff --git a/private/my-local-model/src/XYZServiceClient.ts b/private/my-local-model/src/XYZServiceClient.ts index 3faf55ff8fe..ba247017dd8 100644 --- a/private/my-local-model/src/XYZServiceClient.ts +++ b/private/my-local-model/src/XYZServiceClient.ts @@ -12,11 +12,8 @@ import { import { getContentLengthPlugin } from "@smithy/middleware-content-length"; import { type EndpointInputConfig, - type EndpointRequiredInputConfig, - type EndpointRequiredResolvedConfig, type EndpointResolvedConfig, resolveEndpointConfig, - resolveEndpointRequiredConfig, } from "@smithy/middleware-endpoint"; import { type RetryInputConfig, @@ -189,7 +186,6 @@ export type XYZServiceClientConfigType = Partial<__SmithyConfiguration<__HttpHan ClientDefaults & RetryInputConfig & EndpointInputConfig & - EndpointRequiredInputConfig & EventStreamSerdeInputConfig & HttpAuthSchemeInputConfig & ClientInputEndpointParameters; @@ -208,7 +204,6 @@ export type XYZServiceClientResolvedConfigType = __SmithyResolvedConfiguration<_ RuntimeExtensionsConfig & RetryResolvedConfig & EndpointResolvedConfig & - EndpointRequiredResolvedConfig & EventStreamSerdeResolvedConfig & HttpAuthSchemeResolvedConfig & ClientResolvedEndpointParameters; @@ -241,18 +236,18 @@ export class XYZServiceClient extends __Client< const _config_1 = resolveClientEndpointParameters(_config_0); const _config_2 = resolveRetryConfig(_config_1); const _config_3 = resolveEndpointConfig(_config_2); - const _config_4 = resolveEndpointRequiredConfig(_config_3); - const _config_5 = resolveEventStreamSerdeConfig(_config_4); - const _config_6 = resolveHttpAuthSchemeConfig(_config_5); - const _config_7 = resolveRuntimeExtensions(_config_6, configuration?.extensions || []); - this.config = _config_7; + const _config_4 = resolveEventStreamSerdeConfig(_config_3); + const _config_5 = resolveHttpAuthSchemeConfig(_config_4); + const _config_6 = resolveRuntimeExtensions(_config_5, configuration?.extensions || []); + this.config = _config_6; this.middlewareStack.use(getRetryPlugin(this.config)); this.middlewareStack.use(getContentLengthPlugin(this.config)); this.middlewareStack.use( getHttpAuthSchemeEndpointRuleSetPlugin(this.config, { httpAuthSchemeParametersProvider: defaultXYZServiceHttpAuthSchemeParametersProvider, identityProviderConfigProvider: async (config: XYZServiceClientResolvedConfig) => - new DefaultIdentityProviderConfig({}), + new DefaultIdentityProviderConfig({ + "smithy.api#httpApiKeyAuth": config.apiKey,}), }) ); this.middlewareStack.use(getHttpSigningPlugin(this.config)); diff --git a/private/my-local-model/src/auth/httpAuthExtensionConfiguration.ts b/private/my-local-model/src/auth/httpAuthExtensionConfiguration.ts index dada1d85ed3..c0d91baa32f 100644 --- a/private/my-local-model/src/auth/httpAuthExtensionConfiguration.ts +++ b/private/my-local-model/src/auth/httpAuthExtensionConfiguration.ts @@ -1,5 +1,5 @@ // smithy-typescript generated code -import type { HttpAuthScheme } from "@smithy/types"; +import type { ApiKeyIdentity, ApiKeyIdentityProvider, HttpAuthScheme } from "@smithy/types"; import type { XYZServiceHttpAuthSchemeProvider } from "./httpAuthSchemeProvider"; @@ -11,6 +11,8 @@ export interface HttpAuthExtensionConfiguration { httpAuthSchemes(): HttpAuthScheme[]; setHttpAuthSchemeProvider(httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider): void; httpAuthSchemeProvider(): XYZServiceHttpAuthSchemeProvider; + setApiKey(apiKey: ApiKeyIdentity | ApiKeyIdentityProvider): void; + apiKey(): ApiKeyIdentity | ApiKeyIdentityProvider | undefined; } /** @@ -19,6 +21,7 @@ export interface HttpAuthExtensionConfiguration { export type HttpAuthRuntimeConfig = Partial<{ httpAuthSchemes: HttpAuthScheme[]; httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider; + apiKey: ApiKeyIdentity | ApiKeyIdentityProvider; }>; /** @@ -29,6 +32,7 @@ export const getHttpAuthExtensionConfiguration = ( ): HttpAuthExtensionConfiguration => { const _httpAuthSchemes = runtimeConfig.httpAuthSchemes!; let _httpAuthSchemeProvider = runtimeConfig.httpAuthSchemeProvider!; + let _apiKey = runtimeConfig.apiKey; return { setHttpAuthScheme(httpAuthScheme: HttpAuthScheme): void { const index = _httpAuthSchemes.findIndex((scheme) => scheme.schemeId === httpAuthScheme.schemeId); @@ -47,6 +51,12 @@ export const getHttpAuthExtensionConfiguration = ( httpAuthSchemeProvider(): XYZServiceHttpAuthSchemeProvider { return _httpAuthSchemeProvider; }, + setApiKey(apiKey: ApiKeyIdentity | ApiKeyIdentityProvider): void { + _apiKey = apiKey; + }, + apiKey(): ApiKeyIdentity | ApiKeyIdentityProvider | undefined { + return _apiKey; + }, }; }; @@ -57,5 +67,6 @@ export const resolveHttpAuthRuntimeConfig = (config: HttpAuthExtensionConfigurat return { httpAuthSchemes: config.httpAuthSchemes(), httpAuthSchemeProvider: config.httpAuthSchemeProvider(), + apiKey: config.apiKey(), }; }; diff --git a/private/my-local-model/src/auth/httpAuthSchemeProvider.ts b/private/my-local-model/src/auth/httpAuthSchemeProvider.ts index ff178de097d..c3f6b846ca4 100644 --- a/private/my-local-model/src/auth/httpAuthSchemeProvider.ts +++ b/private/my-local-model/src/auth/httpAuthSchemeProvider.ts @@ -1,12 +1,16 @@ // smithy-typescript generated code -import type { - HandlerExecutionContext, - HttpAuthOption, - HttpAuthScheme, - HttpAuthSchemeParameters, - HttpAuthSchemeParametersProvider, - HttpAuthSchemeProvider, - Provider, +import { doesIdentityRequireRefresh, isIdentityExpired, memoizeIdentityProvider } from "@smithy/core"; +import { + type ApiKeyIdentity, + type ApiKeyIdentityProvider, + type HandlerExecutionContext, + type HttpAuthOption, + type HttpAuthScheme, + type HttpAuthSchemeParameters, + type HttpAuthSchemeParametersProvider, + type HttpAuthSchemeProvider, + type Provider, + HttpApiKeyAuthLocation, } from "@smithy/types"; import { getSmithyContext, normalizeProvider } from "@smithy/util-middleware"; @@ -41,9 +45,14 @@ export const defaultXYZServiceHttpAuthSchemeParametersProvider = async ( }; }; -function createSmithyApiNoAuthHttpAuthOption(authParameters: XYZServiceHttpAuthSchemeParameters): HttpAuthOption { +function createSmithyApiHttpApiKeyAuthHttpAuthOption(authParameters: XYZServiceHttpAuthSchemeParameters): HttpAuthOption { return { - schemeId: "smithy.api#noAuth", + schemeId: "smithy.api#httpApiKeyAuth", + signingProperties: { + name: "X-Api-Key", + in: HttpApiKeyAuthLocation.HEADER, + scheme: undefined, + }, }; } @@ -59,7 +68,7 @@ export const defaultXYZServiceHttpAuthSchemeProvider: XYZServiceHttpAuthSchemePr const options: HttpAuthOption[] = []; switch (authParameters.operation) { default: { - options.push(createSmithyApiNoAuthHttpAuthOption(authParameters)); + options.push(createSmithyApiHttpApiKeyAuthHttpAuthOption(authParameters)); } } return options; @@ -88,6 +97,10 @@ export interface HttpAuthSchemeInputConfig { * @internal */ httpAuthSchemeProvider?: XYZServiceHttpAuthSchemeProvider; + /** + * The API key to use when making requests. + */ + apiKey?: ApiKeyIdentity | ApiKeyIdentityProvider; } /** @@ -113,6 +126,10 @@ export interface HttpAuthSchemeResolvedConfig { * @internal */ readonly httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider; + /** + * The API key to use when making requests. + */ + readonly apiKey?: ApiKeyIdentityProvider; } /** @@ -121,7 +138,9 @@ export interface HttpAuthSchemeResolvedConfig { export const resolveHttpAuthSchemeConfig = ( config: T & HttpAuthSchemeInputConfig ): T & HttpAuthSchemeResolvedConfig => { + const apiKey = memoizeIdentityProvider(config.apiKey, isIdentityExpired, doesIdentityRequireRefresh); return Object.assign(config, { authSchemePreference: normalizeProvider(config.authSchemePreference ?? []), + apiKey, }) as T & HttpAuthSchemeResolvedConfig; }; diff --git a/private/my-local-model/src/endpoint/EndpointParameters.ts b/private/my-local-model/src/endpoint/EndpointParameters.ts index 4bd1abf15e5..3dc9eafeb29 100644 --- a/private/my-local-model/src/endpoint/EndpointParameters.ts +++ b/private/my-local-model/src/endpoint/EndpointParameters.ts @@ -5,7 +5,20 @@ import type { Endpoint, EndpointParameters as __EndpointParameters, EndpointV2, * @public */ export interface ClientInputEndpointParameters { + clientContextParams?: { + apiKey?: string | undefined | Provider; + region?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; + logger?: string | undefined | Provider; + }; endpoint?: string | Provider | Endpoint | Provider | EndpointV2 | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; } /** @@ -15,6 +28,13 @@ export type ClientResolvedEndpointParameters = Omit( options: T & ClientInputEndpointParameters ): T & ClientResolvedEndpointParameters => { return Object.assign(options, { + customParam: options.customParam ?? "default-custom-value", + enableFeature: options.enableFeature ?? true, + debugMode: options.debugMode ?? false, + nonConflictingParam: options.nonConflictingParam ?? "non-conflict-default", defaultSigningName: "", + clientContextParams: Object.assign(clientContextParamDefaults, options.clientContextParams), }); }; @@ -30,6 +55,13 @@ export const resolveClientEndpointParameters = ( * @internal */ export const commonParams = { + ApiKey: { type: "clientContextParams", name: "apiKey" }, + nonConflictingParam: { type: "clientContextParams", name: "nonConflictingParam" }, + logger: { type: "clientContextParams", name: "logger" }, + region: { type: "clientContextParams", name: "region" }, + customParam: { type: "clientContextParams", name: "customParam" }, + debugMode: { type: "clientContextParams", name: "debugMode" }, + enableFeature: { type: "clientContextParams", name: "enableFeature" }, endpoint: { type: "builtInParams", name: "endpoint" }, } as const; @@ -37,5 +69,12 @@ export const commonParams = { * @internal */ export interface EndpointParameters extends __EndpointParameters { - endpoint?: string | undefined; + endpoint: string; + ApiKey?: string | undefined; + region?: string | undefined; + customParam?: string | undefined; + enableFeature?: boolean | undefined; + debugMode?: boolean | undefined; + nonConflictingParam?: string | undefined; + logger?: string | undefined; } diff --git a/private/my-local-model/src/endpoint/endpointResolver.ts b/private/my-local-model/src/endpoint/endpointResolver.ts index aaeb8e220ba..174cbdace53 100644 --- a/private/my-local-model/src/endpoint/endpointResolver.ts +++ b/private/my-local-model/src/endpoint/endpointResolver.ts @@ -7,7 +7,8 @@ import { ruleSet } from "./ruleset"; const cache = new EndpointCache({ size: 50, - params: ["endpoint"], + params: ["ApiKey", + "endpoint"], }); /** diff --git a/private/my-local-model/src/endpoint/ruleset.ts b/private/my-local-model/src/endpoint/ruleset.ts index d8b2ce3d908..99179427a9c 100644 --- a/private/my-local-model/src/endpoint/ruleset.ts +++ b/private/my-local-model/src/endpoint/ruleset.ts @@ -5,9 +5,50 @@ export const ruleSet: RuleSetObject = { version: "1.0", parameters: { endpoint: { - type: "string", builtIn: "SDK::Endpoint", - documentation: "Endpoint used for making requests. Should be formatted as a URI.", + required: true, + documentation: "The endpoint used to send the request.", + type: "string", + }, + ApiKey: { + required: false, + documentation: "ApiKey", + type: "string", + }, + region: { + type: "string", + required: false, + documentation: "AWS region", + }, + customParam: { + type: "string", + required: true, + default: "default-custom-value", + documentation: "Custom parameter for testing", + }, + enableFeature: { + type: "boolean", + required: true, + default: true, + documentation: "Feature toggle with default", + }, + debugMode: { + type: "boolean", + required: true, + default: false, + documentation: "Debug mode with default", + }, + nonConflictingParam: { + type: "string", + required: true, + default: "non-conflict-default", + documentation: "Non-conflicting with default", + }, + logger: { + type: "string", + required: true, + default: "default-logger", + documentation: "Conflicting logger with default", }, }, rules: [ @@ -17,22 +58,30 @@ export const ruleSet: RuleSetObject = { fn: "isSet", argv: [ { - ref: "endpoint", + ref: "ApiKey", }, ], }, ], endpoint: { - url: { - ref: "endpoint", + url: "{endpoint}", + properties: {}, + headers: { + "x-api-key": [ + "{ApiKey}", + ], }, }, type: "endpoint", }, { conditions: [], - error: "(default endpointRuleSet) endpoint is not set - you must configure an endpoint.", - type: "error", + endpoint: { + url: "{endpoint}", + properties: {}, + headers: {}, + }, + type: "endpoint", }, ], }; diff --git a/private/my-local-model/src/runtimeConfig.shared.ts b/private/my-local-model/src/runtimeConfig.shared.ts index 13ac7264c70..a8bf928afeb 100644 --- a/private/my-local-model/src/runtimeConfig.shared.ts +++ b/private/my-local-model/src/runtimeConfig.shared.ts @@ -1,5 +1,5 @@ // smithy-typescript generated code -import { NoAuthSigner } from "@smithy/core"; +import { HttpApiKeyAuthSigner } from "@smithy/core"; import { NoOpLogger } from "@smithy/smithy-client"; import type { IdentityProviderConfig } from "@smithy/types"; import { parseUrl } from "@smithy/url-parser"; @@ -24,10 +24,10 @@ export const getRuntimeConfig = (config: XYZServiceClientConfig) => { httpAuthSchemeProvider: config?.httpAuthSchemeProvider ?? defaultXYZServiceHttpAuthSchemeProvider, httpAuthSchemes: config?.httpAuthSchemes ?? [ { - schemeId: "smithy.api#noAuth", + schemeId: "smithy.api#httpApiKeyAuth", identityProvider: (ipc: IdentityProviderConfig) => - ipc.getIdentityProvider("smithy.api#noAuth") || (async () => ({})), - signer: new NoAuthSigner(), + ipc.getIdentityProvider("smithy.api#httpApiKeyAuth"), + signer: new HttpApiKeyAuthSigner(), }, ], logger: config?.logger ?? new NoOpLogger(), diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/auth/http/integration/SupportHttpApiKeyAuth.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/auth/http/integration/SupportHttpApiKeyAuth.java index 8ce8d9db999..dcf45926657 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/auth/http/integration/SupportHttpApiKeyAuth.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/auth/http/integration/SupportHttpApiKeyAuth.java @@ -36,11 +36,13 @@ public class SupportHttpApiKeyAuth implements HttpAuthTypeScriptIntegration { .name("ApiKeyIdentity") .namespace(TypeScriptDependency.SMITHY_TYPES.getPackageName(), "/") .addDependency(TypeScriptDependency.SMITHY_TYPES) + .putProperty("typeOnly", true) .build(); private static final Symbol API_KEY_IDENTITY_PROVIDER = Symbol.builder() .name("ApiKeyIdentityProvider") .namespace(TypeScriptDependency.SMITHY_TYPES.getPackageName(), "/") .addDependency(TypeScriptDependency.SMITHY_TYPES) + .putProperty("typeOnly", true) .build(); private static final Symbol HTTP_API_KEY_LOCATION = Symbol.builder() .name("HttpApiKeyAuthLocation") diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/ClientConfigKeys.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/ClientConfigKeys.java new file mode 100644 index 00000000000..9556ae38769 --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/ClientConfigKeys.java @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.typescript.codegen.endpointsV2; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Manages known client configuration keys that should not be placed in + * clientContextParams. + */ +@SmithyInternalApi +public final class ClientConfigKeys { + private static final Set KNOWN_CONFIG_KEYS = ConcurrentHashMap.newKeySet(); + + static { + // Initialize with common client config keys + KNOWN_CONFIG_KEYS.add("profile"); + KNOWN_CONFIG_KEYS.add("apiKey"); + KNOWN_CONFIG_KEYS.add("region"); + KNOWN_CONFIG_KEYS.add("credentials"); + KNOWN_CONFIG_KEYS.add("endpoint"); + KNOWN_CONFIG_KEYS.add("cacheMiddleware"); + KNOWN_CONFIG_KEYS.add("requestHandler"); + KNOWN_CONFIG_KEYS.add("retryStrategy"); + KNOWN_CONFIG_KEYS.add("retryMode"); + KNOWN_CONFIG_KEYS.add("maxAttempts"); + KNOWN_CONFIG_KEYS.add("logger"); + KNOWN_CONFIG_KEYS.add("signer"); + KNOWN_CONFIG_KEYS.add("useDualstackEndpoint"); + KNOWN_CONFIG_KEYS.add("useFipsEndpoint"); + KNOWN_CONFIG_KEYS.add("customUserAgent"); + KNOWN_CONFIG_KEYS.add("extensions"); + KNOWN_CONFIG_KEYS.add("tls"); + KNOWN_CONFIG_KEYS.add("disableHostPrefix"); + KNOWN_CONFIG_KEYS.add("signingRegion"); + KNOWN_CONFIG_KEYS.add("sigv4aSigningRegionSet"); + KNOWN_CONFIG_KEYS.add("authSchemePreference"); + KNOWN_CONFIG_KEYS.add("userAgentAppId"); + KNOWN_CONFIG_KEYS.add("protocol"); + KNOWN_CONFIG_KEYS.add("apiVersion"); + KNOWN_CONFIG_KEYS.add("serviceId"); + KNOWN_CONFIG_KEYS.add("runtime"); + KNOWN_CONFIG_KEYS.add("systemClockOffset"); + KNOWN_CONFIG_KEYS.add("signerConstructor"); + KNOWN_CONFIG_KEYS.add("endpointProvider"); + KNOWN_CONFIG_KEYS.add("urlParser"); + KNOWN_CONFIG_KEYS.add("base64Decoder"); + KNOWN_CONFIG_KEYS.add("base64Encoder"); + KNOWN_CONFIG_KEYS.add("defaultsMode"); + KNOWN_CONFIG_KEYS.add("bodyLengthChecker"); + KNOWN_CONFIG_KEYS.add("credentialDefaultProvider"); + KNOWN_CONFIG_KEYS.add("defaultUserAgentProvider"); + KNOWN_CONFIG_KEYS.add("eventStreamSerdeProvider"); + KNOWN_CONFIG_KEYS.add("getAwsChunkedEncodingStream"); + KNOWN_CONFIG_KEYS.add("md5"); + KNOWN_CONFIG_KEYS.add("sdkStreamMixin"); + KNOWN_CONFIG_KEYS.add("sha1"); + KNOWN_CONFIG_KEYS.add("sha256"); + KNOWN_CONFIG_KEYS.add("streamCollector"); + KNOWN_CONFIG_KEYS.add("streamHasher"); + KNOWN_CONFIG_KEYS.add("utf8Decoder"); + KNOWN_CONFIG_KEYS.add("utf8Encoder"); + KNOWN_CONFIG_KEYS.add("httpAuthSchemes"); + KNOWN_CONFIG_KEYS.add("httpAuthSchemeProvider"); + KNOWN_CONFIG_KEYS.add("serviceConfiguredEndpoint"); + } + + private ClientConfigKeys() { + // Utility class + } + + /** + * Add a configuration key to the known set. + * + * @param key the configuration key to add + */ + public static void addConfigKey(String key) { + KNOWN_CONFIG_KEYS.add(key); + } + + /** + * Check if a key is a known configuration key. + * + * @param key the configuration key to check + * @return true if the key is known + */ + public static boolean isKnownConfigKey(String key) { + return KNOWN_CONFIG_KEYS.contains(key); + } + + /** + * Get custom context parameters by filtering out built-in and known config + * keys. + * + * @param clientContextParams all client context parameters + * @param builtInParams built-in parameters + * @return filtered custom context parameters + */ + public static Map getCustomContextParams( + Map clientContextParams, + Map builtInParams + ) { + Map customContextParams = new java.util.HashMap<>(); + for (Map.Entry entry : clientContextParams.entrySet()) { + if (!builtInParams.containsKey(entry.getKey()) + && !KNOWN_CONFIG_KEYS.contains(entry.getKey())) { + customContextParams.put(entry.getKey(), entry.getValue()); + } + } + return customContextParams; + } +} diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2Generator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2Generator.java index c05151530e4..7d025a0f53a 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2Generator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2Generator.java @@ -17,6 +17,7 @@ import java.nio.file.Paths; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -111,21 +112,50 @@ private void generateEndpointParameters() { writer -> { writer.addTypeImport("EndpointParameters", "__EndpointParameters", TypeScriptDependency.SMITHY_TYPES); writer.addTypeImport("Provider", null, TypeScriptDependency.SMITHY_TYPES); + Map clientContextParams = + ruleSetParameterFinder.getClientContextParams(); + Map builtInParams = ruleSetParameterFinder.getBuiltInParams(); + builtInParams.keySet().removeIf(OmitEndpointParams::isOmitted); + Map customContextParams = ClientConfigKeys.getCustomContextParams( + clientContextParams, builtInParams + ); writer.writeDocs("@public"); writer.openBlock( "export interface ClientInputEndpointParameters {", "}", () -> { - Map clientInputParams = ruleSetParameterFinder.getClientContextParams(); - //Omit Endpoint params that should not be a part of the ClientInputEndpointParameters interface - Map builtInParams = ruleSetParameterFinder.getBuiltInParams(); - builtInParams.keySet().removeIf(OmitEndpointParams::isOmitted); - clientInputParams.putAll(builtInParams); - + // Only include client context params that are NOT built-ins + Map clientContextParamsExcludingBuiltIns = new HashMap<>(clientContextParams); + clientContextParamsExcludingBuiltIns.keySet().removeAll(builtInParams.keySet()); + if (!clientContextParamsExcludingBuiltIns.isEmpty()) { + writer.write("clientContextParams?: {"); + writer.indent(); + ObjectNode ruleSet = endpointRuleSetTrait.getRuleSet().expectObjectNode(); + ruleSet.getObjectMember("parameters").ifPresent(parameters -> { + parameters.accept(new RuleSetParametersVisitor(writer, + clientContextParamsExcludingBuiltIns, true)); + }); + writer.dedent(); + writer.write("};"); + } + // Add direct params (built-ins + custom context params, excluding conflicting) + Map directParams = new HashMap<>(); + // Add all built-ins (they should always be at root level, even if conflicting) + directParams.putAll(builtInParams); + // Add custom context params excluding conflicting ones + customContextParams.entrySet().forEach(entry -> { + String paramName = entry.getKey(); + String localName = EndpointsParamNameMap + .getLocalName(paramName); + if (!ClientConfigKeys.isKnownConfigKey(paramName) + && !ClientConfigKeys.isKnownConfigKey(localName)) { + directParams.put(paramName, entry.getValue()); + } + }); ObjectNode ruleSet = endpointRuleSetTrait.getRuleSet().expectObjectNode(); ruleSet.getObjectMember("parameters").ifPresent(parameters -> { - parameters.accept(new RuleSetParametersVisitor(writer, clientInputParams, true)); + parameters.accept(new RuleSetParametersVisitor(writer, directParams, true)); }); } ); @@ -138,6 +168,9 @@ private void generateEndpointParameters() { defaultSigningName: string; };""" ); + if (ruleSetParameterFinder.hasCustomClientContextParams()) { + ruleSetParameterFinder.writeNestedClientContextParamDefaults(writer); + } writer.write(""); writer.writeDocs("@internal"); @@ -157,6 +190,9 @@ private void generateEndpointParameters() { "defaultSigningName: \"$L\",", settings.getDefaultSigningName() ); + if (ruleSetParameterFinder.hasCustomClientContextParams()) { + ruleSetParameterFinder.writeConfigResolverNestedClientContextParams(writer); + } }); } ); diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParameterFinder.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParameterFinder.java index 2c6235f8823..a34a3f8d1d2 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParameterFinder.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParameterFinder.java @@ -50,6 +50,7 @@ import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait; import software.amazon.smithy.rulesengine.traits.OperationContextParamsTrait; import software.amazon.smithy.rulesengine.traits.StaticContextParamsTrait; +import software.amazon.smithy.typescript.codegen.TypeScriptWriter; import software.amazon.smithy.utils.SmithyInternalApi; @SmithyInternalApi @@ -173,6 +174,101 @@ public Map getBuiltInParams() { return map; } + /** + * Check if there are custom client context parameters. + */ + public boolean hasCustomClientContextParams() { + Map clientContextParams = getClientContextParams(); + Map builtInParams = getBuiltInParams(); + builtInParams.keySet().removeIf(OmitEndpointParams::isOmitted); + Map customContextParams = ClientConfigKeys.getCustomContextParams( + clientContextParams, builtInParams + ); + return !customContextParams.isEmpty(); + } + + /** + * Write custom client context parameters to TypeScript writer. + */ + public void writeInputConfigCustomClientContextParams(TypeScriptWriter writer) { + Map clientContextParams = getClientContextParams(); + Map builtInParams = getBuiltInParams(); + builtInParams.keySet().removeIf(OmitEndpointParams::isOmitted); + Map customContextParams = ClientConfigKeys.getCustomContextParams( + clientContextParams, builtInParams + ); + ObjectNode ruleSet = ruleset.getRuleSet().expectObjectNode(); + ruleSet.getObjectMember("parameters").ifPresent(parameters -> { + parameters.accept(new RuleSetParametersVisitor(writer, customContextParams, true)); + }); + } + + /** + * Write nested client context parameter defaults to TypeScript writer. + * Only includes conflicting parameters with default values. + */ + public void writeNestedClientContextParamDefaults(TypeScriptWriter writer) { + Map clientContextParams = getClientContextParams(); + Map builtInParams = getBuiltInParams(); + builtInParams.keySet().removeIf(OmitEndpointParams::isOmitted); + ObjectNode ruleSet = ruleset.getRuleSet().expectObjectNode(); + if (ruleSet.getObjectMember("parameters").isPresent()) { + ObjectNode parameters = ruleSet.getObjectMember("parameters").get().expectObjectNode(); + writer.write(""); + writer.writeDocs("@internal"); + writer.openBlock("const clientContextParamDefaults = {", "} as const;", () -> { + // Write defaults only for conflicting parameters + for (Map.Entry entry : clientContextParams.entrySet()) { + String paramName = entry.getKey(); + // Check if this is a conflicting parameter (exists in both clientContextParams and knownConfigKeys) + if (ClientConfigKeys.isKnownConfigKey(paramName) && !builtInParams.containsKey(paramName)) { + ObjectNode paramNode = parameters.getObjectMember(paramName).orElse(null); + if (paramNode != null && paramNode.containsMember("default")) { + software.amazon.smithy.model.node.Node defaultValue = paramNode.getMember("default").get(); + if (defaultValue.isStringNode()) { + writer.write("$L: \"$L\",", paramName, defaultValue.expectStringNode().getValue()); + } else if (defaultValue.isBooleanNode()) { + writer.write("$L: $L,", paramName, defaultValue.expectBooleanNode().getValue()); + } + } + } + } + }); + } + } + + /** + * Write config resolver nested client context parameters to TypeScript writer. + */ + public void writeConfigResolverNestedClientContextParams(TypeScriptWriter writer) { + Map clientContextParams = getClientContextParams(); + Map builtInParams = getBuiltInParams(); + builtInParams.keySet().removeIf(OmitEndpointParams::isOmitted); + Map customContextParams = ClientConfigKeys.getCustomContextParams( + clientContextParams, builtInParams + ); + ObjectNode ruleSet = ruleset.getRuleSet().expectObjectNode(); + boolean hasDefaultsForResolve = false; + if (ruleSet.getObjectMember("parameters").isPresent()) { + ObjectNode parameters = ruleSet.getObjectMember("parameters").get().expectObjectNode(); + hasDefaultsForResolve = customContextParams.entrySet().stream() + .anyMatch(entry -> { + ObjectNode paramNode = parameters.getObjectMember(entry.getKey()).orElse(null); + return paramNode != null && paramNode.containsMember("default"); + }); + } + if (hasDefaultsForResolve) { + writer.write( + "clientContextParams: Object.assign(clientContextParamDefaults, " + + "options.clientContextParams)," + ); + } else { + writer.write( + "clientContextParams: options.clientContextParams ?? {}," + ); + } + } + /** * Defined on the service shape as smithy.rules#clientContextParams traits. */ diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParametersVisitor.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParametersVisitor.java index 6fd7937246b..4361d3e3b2a 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParametersVisitor.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/RuleSetParametersVisitor.java @@ -73,7 +73,10 @@ public Void objectNode(ObjectNode node) { if (writeDefaults) { if (parameterGenerator.hasDefault()) { - writer.write(parameterGenerator.defaultAsCodeString()); + // Don't write root-level defaults for conflicting parameters + if (!ClientConfigKeys.isKnownConfigKey(key)) { + writer.write(parameterGenerator.defaultAsCodeString()); + } } } else if (clientContextParams.isEmpty() || clientContextParams.containsKey(key)) { boolean isClientContextParams = !clientContextParams.isEmpty(); diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2GeneratorTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2GeneratorTest.java index 2a1a9550674..a4ba8925fbb 100644 --- a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2GeneratorTest.java +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/endpointsV2/EndpointsV2GeneratorTest.java @@ -154,14 +154,19 @@ public void containsExtraContextParameter() { return Object.assign(options, { stage: options.stage ?? "production", defaultSigningName: "", + clientContextParams: Object.assign(clientContextParamDefaults, options.clientContextParams), }); """)); assertThat(endpointParameters, containsString( """ export interface ClientInputEndpointParameters { - region?: string | undefined | Provider; + clientContextParams?: { + region?: string | undefined | Provider; + stage?: string | undefined | Provider; + }; stage?: string | undefined | Provider; - endpoint?:""")); + endpoint?: string | Provider | Endpoint | Provider | EndpointV2 | Provider; + }""")); } private MockManifest testEndpoints(String filename) { diff --git a/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy b/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy index 9eb591a0f1c..59434fd81bd 100644 --- a/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy +++ b/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy @@ -3,9 +3,65 @@ $version: "2.0" namespace org.xyz.v1 use smithy.protocols#rpcv2Cbor +use smithy.rules#clientContextParams +use smithy.rules#endpointRuleSet @rpcv2Cbor @documentation("xyz interfaces") +@httpApiKeyAuth(name: "X-Api-Key", in: "header") +@clientContextParams( + customParam: { type: "string", documentation: "Custom parameter" } + region: { type: "string", documentation: "Conflicting with built-in region" } + enableFeature: { type: "boolean", documentation: "Feature toggle flag" } + debugMode: { type: "boolean", documentation: "Debug mode flag" } + nonConflictingParam: { type: "string", documentation: "Non-conflicting parameter" } + logger: { type: "string", documentation: "Conflicting logger parameter" } + ApiKey: { type: "string", documentation: "ApiKey" } +) +@endpointRuleSet({ + version: "1.0" + parameters: { + endpoint: { builtIn: "SDK::Endpoint", required: true, documentation: "The endpoint used to send the request.", type: "string" } + ApiKey: { required: false, documentation: "ApiKey", type: "string" } + region: { type: "string", required: false, documentation: "AWS region" } + customParam: { type: "string", required: true, default: "default-custom-value", documentation: "Custom parameter for testing" } + enableFeature: { type: "boolean", required: true, default: true, documentation: "Feature toggle with default" } + debugMode: { type: "boolean", required: true, default: false, documentation: "Debug mode with default" } + nonConflictingParam: { type: "string", required: true, default: "non-conflict-default", documentation: "Non-conflicting with default" } + logger: { type: "string", required: true, default: "default-logger", documentation: "Conflicting logger with default" } + } + rules: [ + { + conditions: [ + { + fn: "isSet" + argv: [ + { + ref: "ApiKey" + } + ] + } + ] + endpoint: { + url: "{endpoint}" + properties: {} + headers: { + "x-api-key": ["{ApiKey}"] + } + } + type: "endpoint" + } + { + conditions: [] + endpoint: { + url: "{endpoint}" + properties: {} + headers: {} + } + type: "endpoint" + } + ] +}) service XYZService { version: "1.0" operations: [