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/eventstream-cbor.integ.spec.ts b/packages/eventstream-serde-universal/src/eventstream-cbor.integ.spec.ts index f509228a692..23b84258f97 100644 --- a/packages/eventstream-serde-universal/src/eventstream-cbor.integ.spec.ts +++ b/packages/eventstream-serde-universal/src/eventstream-cbor.integ.spec.ts @@ -9,7 +9,8 @@ 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: { apiKey: "test-api-key" }, + } as any); const body = cbor.serialize({ id: "alpha", diff --git a/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.spec.ts b/packages/middleware-endpoint/src/adaptors/createConfigValueProvider.spec.ts index 1c7e6a06bd1..2fc7839fcfb 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 = { + apiKey: "direct-api-key", + clientContextParams: { + apiKey: "nested-api-key", + }, + }; + expect(await createConfigValueProvider("apiKey", "apiKey", config, true)()).toEqual("nested-api-key"); + }); + + 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..8fc324c3206 100644 --- a/packages/util-retry/src/retries.integ.spec.ts +++ b/packages/util-retry/src/retries.integ.spec.ts @@ -20,7 +20,8 @@ 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" }, + } as any); requireRequestsFrom(client) .toMatch({ @@ -50,7 +51,8 @@ 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" }, + } as any); requireRequestsFrom(client) .toMatch({ @@ -80,7 +82,8 @@ describe("retries", () => { it("should retry retryable trait with throttling", async () => { const client = new XYZService({ endpoint: "https://localhost/nowhere", - }); + apiKey: { apiKey: "test-api-key" }, + } as any); requireRequestsFrom(client) .toMatch({ @@ -110,7 +113,8 @@ 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" }, + } as any); requireRequestsFrom(client) .toMatch({ diff --git a/private/my-local-model-schema/src/XYZServiceClient.ts b/private/my-local-model-schema/src/XYZServiceClient.ts index 8e0a2ddfd38..d4a06dd9e42 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,9 @@ 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..3238b9a1c52 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 HttpAuthScheme, ApiKeyIdentity, ApiKeyIdentityProvider } 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..9c940463d5d 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 HandlerExecutionContext, + type HttpAuthOption, + type HttpAuthScheme, + type HttpAuthSchemeParameters, + type HttpAuthSchemeParametersProvider, + type HttpAuthSchemeProvider, + type Provider, + ApiKeyIdentity, + ApiKeyIdentityProvider, + HttpApiKeyAuthLocation, } from "@smithy/types"; import { getSmithyContext, normalizeProvider } from "@smithy/util-middleware"; @@ -41,9 +45,16 @@ 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 +70,7 @@ export const defaultXYZServiceHttpAuthSchemeProvider: XYZServiceHttpAuthSchemePr const options: HttpAuthOption[] = []; switch (authParameters.operation) { default: { - options.push(createSmithyApiNoAuthHttpAuthOption(authParameters)); + options.push(createSmithyApiHttpApiKeyAuthHttpAuthOption(authParameters)); } } return options; @@ -88,6 +99,11 @@ export interface HttpAuthSchemeInputConfig { * @internal */ httpAuthSchemeProvider?: XYZServiceHttpAuthSchemeProvider; + + /** + * The API key to use when making requests. + */ + apiKey?: ApiKeyIdentity | ApiKeyIdentityProvider; } /** @@ -113,6 +129,11 @@ export interface HttpAuthSchemeResolvedConfig { * @internal */ readonly httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider; + + /** + * The API key to use when making requests. + */ + readonly apiKey?: ApiKeyIdentityProvider; } /** @@ -121,7 +142,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..ef2a870196e 100644 --- a/private/my-local-model-schema/src/endpoint/EndpointParameters.ts +++ b/private/my-local-model-schema/src/endpoint/EndpointParameters.ts @@ -5,16 +5,45 @@ import type { Endpoint, EndpointParameters as __EndpointParameters, EndpointV2, * @public */ export interface ClientInputEndpointParameters { + clientContextParams?: { + apiKey?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; + }; endpoint?: string | Provider | Endpoint | Provider | EndpointV2 | Provider; + apiKey?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; } -/** - * @public - */ -export type ClientResolvedEndpointParameters = Omit & { +export type ClientResolvedEndpointParameters = Omit< + ClientInputEndpointParameters, + "endpoint" | "clientContextParams" +> & { defaultSigningName: string; + clientContextParams: { + apiKey?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; + }; }; +/** + * @internal + */ +const clientContextParamDefaults = { + nonConflictingParam: "non-conflict-default", + customParam: "default-custom-value", + debugMode: false, + enableFeature: true, +} as const; + /** * @internal */ @@ -22,7 +51,12 @@ export const resolveClientEndpointParameters = ( 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 +64,12 @@ export const resolveClientEndpointParameters = ( * @internal */ export const commonParams = { + ApiKey: { type: "clientContextParams", name: "apiKey" }, + nonConflictingParam: { type: "clientContextParams", name: "nonConflictingParam" }, + 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 +77,11 @@ 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; } diff --git a/private/my-local-model-schema/src/endpoint/endpointResolver.ts b/private/my-local-model-schema/src/endpoint/endpointResolver.ts index aaeb8e220ba..fc38cd30897 100644 --- a/private/my-local-model-schema/src/endpoint/endpointResolver.ts +++ b/private/my-local-model-schema/src/endpoint/endpointResolver.ts @@ -7,7 +7,7 @@ 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..24633f95505 100644 --- a/private/my-local-model-schema/src/endpoint/ruleset.ts +++ b/private/my-local-model-schema/src/endpoint/ruleset.ts @@ -5,9 +5,44 @@ 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", }, }, rules: [ @@ -17,22 +52,28 @@ 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..35eccbd1c91 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,9 @@ export const getRuntimeConfig = (config: XYZServiceClientConfig) => { httpAuthSchemeProvider: config?.httpAuthSchemeProvider ?? defaultXYZServiceHttpAuthSchemeProvider, httpAuthSchemes: config?.httpAuthSchemes ?? [ { - schemeId: "smithy.api#noAuth", - identityProvider: (ipc: IdentityProviderConfig) => - ipc.getIdentityProvider("smithy.api#noAuth") || (async () => ({})), - signer: new NoAuthSigner(), + schemeId: "smithy.api#httpApiKeyAuth", + identityProvider: (ipc: IdentityProviderConfig) => 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..a2193e40d05 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,19 @@ 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..3238b9a1c52 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 HttpAuthScheme, ApiKeyIdentity, ApiKeyIdentityProvider } 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..9c940463d5d 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 HandlerExecutionContext, + type HttpAuthOption, + type HttpAuthScheme, + type HttpAuthSchemeParameters, + type HttpAuthSchemeParametersProvider, + type HttpAuthSchemeProvider, + type Provider, + ApiKeyIdentity, + ApiKeyIdentityProvider, + HttpApiKeyAuthLocation, } from "@smithy/types"; import { getSmithyContext, normalizeProvider } from "@smithy/util-middleware"; @@ -41,9 +45,16 @@ 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 +70,7 @@ export const defaultXYZServiceHttpAuthSchemeProvider: XYZServiceHttpAuthSchemePr const options: HttpAuthOption[] = []; switch (authParameters.operation) { default: { - options.push(createSmithyApiNoAuthHttpAuthOption(authParameters)); + options.push(createSmithyApiHttpApiKeyAuthHttpAuthOption(authParameters)); } } return options; @@ -88,6 +99,11 @@ export interface HttpAuthSchemeInputConfig { * @internal */ httpAuthSchemeProvider?: XYZServiceHttpAuthSchemeProvider; + + /** + * The API key to use when making requests. + */ + apiKey?: ApiKeyIdentity | ApiKeyIdentityProvider; } /** @@ -113,6 +129,11 @@ export interface HttpAuthSchemeResolvedConfig { * @internal */ readonly httpAuthSchemeProvider: XYZServiceHttpAuthSchemeProvider; + + /** + * The API key to use when making requests. + */ + readonly apiKey?: ApiKeyIdentityProvider; } /** @@ -121,7 +142,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..ef2a870196e 100644 --- a/private/my-local-model/src/endpoint/EndpointParameters.ts +++ b/private/my-local-model/src/endpoint/EndpointParameters.ts @@ -5,16 +5,45 @@ import type { Endpoint, EndpointParameters as __EndpointParameters, EndpointV2, * @public */ export interface ClientInputEndpointParameters { + clientContextParams?: { + apiKey?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; + }; endpoint?: string | Provider | Endpoint | Provider | EndpointV2 | Provider; + apiKey?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; } -/** - * @public - */ -export type ClientResolvedEndpointParameters = Omit & { +export type ClientResolvedEndpointParameters = Omit< + ClientInputEndpointParameters, + "endpoint" | "clientContextParams" +> & { defaultSigningName: string; + clientContextParams: { + apiKey?: string | undefined | Provider; + customParam?: string | undefined | Provider; + enableFeature?: boolean | undefined | Provider; + debugMode?: boolean | undefined | Provider; + nonConflictingParam?: string | undefined | Provider; + }; }; +/** + * @internal + */ +const clientContextParamDefaults = { + nonConflictingParam: "non-conflict-default", + customParam: "default-custom-value", + debugMode: false, + enableFeature: true, +} as const; + /** * @internal */ @@ -22,7 +51,12 @@ export const resolveClientEndpointParameters = ( 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 +64,12 @@ export const resolveClientEndpointParameters = ( * @internal */ export const commonParams = { + ApiKey: { type: "clientContextParams", name: "apiKey" }, + nonConflictingParam: { type: "clientContextParams", name: "nonConflictingParam" }, + 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 +77,11 @@ 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; } diff --git a/private/my-local-model/src/endpoint/endpointResolver.ts b/private/my-local-model/src/endpoint/endpointResolver.ts index aaeb8e220ba..fc38cd30897 100644 --- a/private/my-local-model/src/endpoint/endpointResolver.ts +++ b/private/my-local-model/src/endpoint/endpointResolver.ts @@ -7,7 +7,7 @@ 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..24633f95505 100644 --- a/private/my-local-model/src/endpoint/ruleset.ts +++ b/private/my-local-model/src/endpoint/ruleset.ts @@ -5,9 +5,44 @@ 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", }, }, rules: [ @@ -17,22 +52,28 @@ 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..10bbdc48319 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,9 @@ export const getRuntimeConfig = (config: XYZServiceClientConfig) => { httpAuthSchemeProvider: config?.httpAuthSchemeProvider ?? defaultXYZServiceHttpAuthSchemeProvider, httpAuthSchemes: config?.httpAuthSchemes ?? [ { - schemeId: "smithy.api#noAuth", - identityProvider: (ipc: IdentityProviderConfig) => - ipc.getIdentityProvider("smithy.api#noAuth") || (async () => ({})), - signer: new NoAuthSigner(), + schemeId: "smithy.api#httpApiKeyAuth", + identityProvider: (ipc: IdentityProviderConfig) => ipc.getIdentityProvider("smithy.api#httpApiKeyAuth"), + signer: new HttpApiKeyAuthSigner(), }, ], logger: config?.logger ?? new NoOpLogger(), diff --git a/private/smithy-rpcv2-cbor-schema/src/endpoint/EndpointParameters.ts b/private/smithy-rpcv2-cbor-schema/src/endpoint/EndpointParameters.ts index 4bd1abf15e5..e0e0831a305 100644 --- a/private/smithy-rpcv2-cbor-schema/src/endpoint/EndpointParameters.ts +++ b/private/smithy-rpcv2-cbor-schema/src/endpoint/EndpointParameters.ts @@ -8,9 +8,6 @@ export interface ClientInputEndpointParameters { endpoint?: string | Provider | Endpoint | Provider | EndpointV2 | Provider; } -/** - * @public - */ export type ClientResolvedEndpointParameters = Omit & { defaultSigningName: string; }; diff --git a/private/smithy-rpcv2-cbor/src/endpoint/EndpointParameters.ts b/private/smithy-rpcv2-cbor/src/endpoint/EndpointParameters.ts index 4bd1abf15e5..e0e0831a305 100644 --- a/private/smithy-rpcv2-cbor/src/endpoint/EndpointParameters.ts +++ b/private/smithy-rpcv2-cbor/src/endpoint/EndpointParameters.ts @@ -8,9 +8,6 @@ export interface ClientInputEndpointParameters { endpoint?: string | Provider | Endpoint | Provider | EndpointV2 | Provider; } -/** - * @public - */ export type ClientResolvedEndpointParameters = Omit & { defaultSigningName: string; }; 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..f280444f57e --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/endpointsV2/ClientConfigKeys.java @@ -0,0 +1,106 @@ +/* + * 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("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); + } + + /** + * 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..591a5c78a3c 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,31 @@ 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); - + if (ruleSetParameterFinder.hasCustomClientContextParams()) { + writer.write("clientContextParams?: {"); + writer.indent(); + ruleSetParameterFinder.writeInputConfigCustomClientContextParams(writer); + writer.dedent(); + writer.write("};"); + } + // Add direct params (built-ins + custom context params) + Map directParams = new HashMap<>(builtInParams); + directParams.putAll(customContextParams); 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 +149,9 @@ private void generateEndpointParameters() { defaultSigningName: string; };""" ); + if (ruleSetParameterFinder.hasCustomClientContextParams()) { + ruleSetParameterFinder.writeNestedClientContextParamDefaults(writer); + } writer.write(""); writer.writeDocs("@internal"); @@ -157,6 +171,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..87784280ef9 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,102 @@ 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. + */ + public void writeNestedClientContextParamDefaults(TypeScriptWriter writer) { + Map clientContextParams = getClientContextParams(); + Map builtInParams = getBuiltInParams(); + builtInParams.keySet().removeIf(OmitEndpointParams::isOmitted); + Map customContextParams = ClientConfigKeys.getCustomContextParams( + clientContextParams, builtInParams); + ObjectNode ruleSet = ruleset.getRuleSet().expectObjectNode(); + if (ruleSet.getObjectMember("parameters").isPresent()) { + ObjectNode parameters = ruleSet.getObjectMember("parameters").get().expectObjectNode(); + boolean hasDefaults = customContextParams.entrySet().stream() + .anyMatch(entry -> { + ObjectNode paramNode = parameters.getObjectMember(entry.getKey()).orElse(null); + return paramNode != null && paramNode.containsMember("default"); + }); + if (hasDefaults) { + writer.write(""); + writer.writeDocs("@internal"); + writer.openBlock("const clientContextParamDefaults = {", "} as const;", () -> { + for (Map.Entry entry : customContextParams.entrySet()) { + String paramName = entry.getKey(); + 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/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..61ae59158e6 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,11 +154,16 @@ public void containsExtraContextParameter() { return Object.assign(options, { stage: options.stage ?? "production", defaultSigningName: "", + clientContextParams: Object.assign(clientContextParamDefaults, options.clientContextParams), }); """)); assertThat(endpointParameters, containsString( """ export interface ClientInputEndpointParameters { + clientContextParams?: { + region?: string | undefined | Provider; + stage?: string | undefined | Provider; + }; region?: string | undefined | Provider; stage?: string | undefined | Provider; endpoint?:""")); 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 c03a53a517e..25dcf57d864 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,63 @@ $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" } + 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" } + } + 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: [