Skip to content

chore: enforce singular config object for resolver stack #1552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/weak-poems-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@smithy/eventstream-serde-config-resolver": minor
"@smithy/middleware-apply-body-checksum": minor
"@smithy/middleware-compression": minor
"@smithy/middleware-endpoint": minor
"@smithy/middleware-retry": minor
"@smithy/config-resolver": minor
"@smithy/protocol-http": minor
"@smithy/smithy-client": minor
"@smithy/util-stream": minor
"@smithy/types": minor
---

enforce singular config object during client instantiation
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ describe(resolveCustomEndpointsConfig.name, () => {

beforeEach(() => {
vi.mocked(normalizeProvider).mockImplementation((input) =>
typeof input === "function" ? input : () => Promise.resolve(input)
typeof input === "function" ? (input as any) : () => Promise.resolve(input)
);
});

afterEach(() => {
vi.clearAllMocks();
});

it("maintains object custody", () => {
const input = { ...mockInput };
expect(resolveCustomEndpointsConfig(input)).toBe(input);
});

describe("tls", () => {
afterEach(() => {
expect(normalizeProvider).toHaveBeenCalledTimes(2);
Expand All @@ -44,7 +49,7 @@ describe(resolveCustomEndpointsConfig.name, () => {
});

it("returns true for isCustomEndpoint", () => {
expect(resolveCustomEndpointsConfig(mockInput).isCustomEndpoint).toStrictEqual(true);
expect(resolveCustomEndpointsConfig({ ...mockInput }).isCustomEndpoint).toStrictEqual(true);
});

it("returns false when useDualstackEndpoint is not defined", async () => {
Expand All @@ -56,23 +61,25 @@ describe(resolveCustomEndpointsConfig.name, () => {
});

describe("returns normalized endpoint", () => {
afterEach(() => {
expect(normalizeProvider).toHaveBeenCalledTimes(2);
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.endpoint);
expect(normalizeProvider).toHaveBeenNthCalledWith(2, mockInput.useDualstackEndpoint);
});

it("calls urlParser endpoint is of type string", async () => {
const mockEndpointString = "http://localhost/";
const endpoint = await resolveCustomEndpointsConfig({ ...mockInput, endpoint: mockEndpointString }).endpoint();
expect(endpoint).toStrictEqual(mockEndpoint);
expect(mockInput.urlParser).toHaveBeenCalledWith(mockEndpointString);

expect(normalizeProvider).toHaveBeenCalledTimes(2);
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.endpoint);
expect(normalizeProvider).toHaveBeenNthCalledWith(2, mockInput.useDualstackEndpoint);
});

it("passes endpoint to normalize if not string", async () => {
const endpoint = await resolveCustomEndpointsConfig(mockInput).endpoint();
const endpoint = await resolveCustomEndpointsConfig({ ...mockInput }).endpoint();
expect(endpoint).toStrictEqual(mockEndpoint);
expect(mockInput.urlParser).not.toHaveBeenCalled();

expect(normalizeProvider).toHaveBeenCalledTimes(2);
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.endpoint);
expect(normalizeProvider).toHaveBeenNthCalledWith(2, mockInput.useDualstackEndpoint);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,11 @@ export interface CustomEndpointsResolvedConfig extends EndpointsResolvedConfig {
export const resolveCustomEndpointsConfig = <T>(
input: T & CustomEndpointsInputConfig & PreviouslyResolved
): T & CustomEndpointsResolvedConfig => {
const { endpoint, urlParser } = input;
return {
...input,
tls: input.tls ?? true,
const { tls, endpoint, urlParser, useDualstackEndpoint } = input;
return Object.assign(input, {
tls: tls ?? true,
endpoint: normalizeProvider(typeof endpoint === "string" ? urlParser(endpoint) : endpoint),
isCustomEndpoint: true,
useDualstackEndpoint: normalizeProvider(input.useDualstackEndpoint ?? false),
};
useDualstackEndpoint: normalizeProvider(useDualstackEndpoint ?? false),
} as CustomEndpointsResolvedConfig);
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@ describe(resolveEndpointsConfig.name, () => {
beforeEach(() => {
vi.mocked(getEndpointFromRegion).mockResolvedValueOnce(mockEndpoint);
vi.mocked(normalizeProvider).mockImplementation((input) =>
typeof input === "function" ? input : () => Promise.resolve(input)
typeof input === "function" ? (input as any) : () => Promise.resolve(input)
);
});

afterEach(() => {
vi.clearAllMocks();
});

it("maintains object custody", () => {
const input = { ...mockInput };
expect(resolveEndpointsConfig(input)).toBe(input);
});

describe("tls", () => {
afterEach(() => {
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.useDualstackEndpoint);
Expand All @@ -53,7 +58,7 @@ describe(resolveEndpointsConfig.name, () => {
});

it("returns true when endpoint is defined", () => {
expect(resolveEndpointsConfig(mockInput).isCustomEndpoint).toStrictEqual(true);
expect(resolveEndpointsConfig({ ...mockInput }).isCustomEndpoint).toStrictEqual(true);
});

it("returns false when endpoint is not defined", () => {
Expand Down Expand Up @@ -90,7 +95,7 @@ describe(resolveEndpointsConfig.name, () => {
});

it("passes endpoint to normalize if not string", async () => {
const endpoint = await resolveEndpointsConfig(mockInput).endpoint();
const endpoint = await resolveEndpointsConfig({ ...mockInput }).endpoint();
expect(endpoint).toStrictEqual(mockEndpoint);
expect(mockInput.urlParser).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,13 @@ export const resolveEndpointsConfig = <T>(
input: T & EndpointsInputConfig & PreviouslyResolved
): T & EndpointsResolvedConfig => {
const useDualstackEndpoint = normalizeProvider(input.useDualstackEndpoint ?? false);
const { endpoint, useFipsEndpoint, urlParser } = input;
return {
...input,
tls: input.tls ?? true,
const { endpoint, useFipsEndpoint, urlParser, tls } = input;
return Object.assign(input, {
tls: tls ?? true,
endpoint: endpoint
? normalizeProvider(typeof endpoint === "string" ? urlParser(endpoint) : endpoint)
: () => getEndpointFromRegion({ ...input, useDualstackEndpoint, useFipsEndpoint }),
isCustomEndpoint: !!endpoint,
useDualstackEndpoint,
};
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ describe("RegionConfig", () => {
vi.clearAllMocks();
});

it("maintains object custody", () => {
const input = {
region: "us-east-1",
};
expect(resolveRegionConfig(input)).toBe(input);
});

describe("region", () => {
it("return normalized value with real region if passed as a string", async () => {
const resolvedRegionConfig = resolveRegionConfig({ region: mockRegion, useFipsEndpoint: mockUseFipsEndpoint });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export const resolveRegionConfig = <T>(input: T & RegionInputConfig & Previously
throw new Error("Region is missing");
}

return {
...input,
return Object.assign(input, {
region: async () => {
if (typeof region === "string") {
return getRealRegion(region);
Expand All @@ -61,5 +60,5 @@ export const resolveRegionConfig = <T>(input: T & RegionInputConfig & Previously
}
return typeof useFipsEndpoint !== "function" ? Promise.resolve(!!useFipsEndpoint) : useFipsEndpoint();
},
};
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ describe("resolveEventStreamSerdeConfig", () => {
vi.clearAllMocks();
});

it("maintains object custody", () => {
const input = {
eventStreamSerdeProvider: vi.fn(),
};
expect(resolveEventStreamSerdeConfig(input)).toBe(input);
});

it("sets value returned by eventStreamSerdeProvider", () => {
const mockReturn = "mockReturn";
eventStreamSerdeProvider.mockReturnValueOnce(mockReturn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface PreviouslyResolved {
*/
export const resolveEventStreamSerdeConfig = <T>(
input: T & PreviouslyResolved & EventStreamSerdeInputConfig
): T & EventStreamSerdeResolvedConfig => ({
...input,
eventStreamMarshaller: input.eventStreamSerdeProvider(input),
});
): T & EventStreamSerdeResolvedConfig =>
Object.assign(input, {
eventStreamMarshaller: input.eventStreamSerdeProvider(input),
});
13 changes: 11 additions & 2 deletions packages/middleware-apply-body-checksum/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { describe, expect, test as it } from "vitest";
import { describe, expect, test as it, vi } from "vitest";

import { applyMd5BodyChecksumMiddleware } from "./index";
import { applyMd5BodyChecksumMiddleware, resolveMd5BodyChecksumConfig } from "./index";

describe("middleware-apply-body-checksum package exports", () => {
it("maintains object custody", () => {
const input = {
md5: vi.fn(),
base64Encoder: vi.fn(),
streamHasher: vi.fn(),
};
expect(resolveMd5BodyChecksumConfig(input)).toBe(input);
});

it("applyMd5BodyChecksumMiddleware", () => {
expect(typeof applyMd5BodyChecksumMiddleware).toBe("function");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ describe(resolveCompressionConfig.name, () => {
requestMinCompressionSizeBytes: 0,
};

it("maintains object custody", () => {
const input = {
disableRequestCompression: false,
requestMinCompressionSizeBytes: 10_000,
};
expect(resolveCompressionConfig(input)).toBe(input);
});

it("should throw an error if requestMinCompressionSizeBytes is less than 0", async () => {
const requestMinCompressionSizeBytes = -1;
const resolvedConfig = resolveCompressionConfig({ ...mockConfig, requestMinCompressionSizeBytes });
Expand Down
34 changes: 18 additions & 16 deletions packages/middleware-compression/src/resolveCompressionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@ import { CompressionInputConfig, CompressionResolvedConfig } from "./configurati
*/
export const resolveCompressionConfig = <T>(
input: T & Required<CompressionInputConfig>
): T & CompressionResolvedConfig => ({
...input,
disableRequestCompression: normalizeProvider(input.disableRequestCompression),
requestMinCompressionSizeBytes: async () => {
const requestMinCompressionSizeBytes = await normalizeProvider(input.requestMinCompressionSizeBytes)();
): T & CompressionResolvedConfig => {
const { disableRequestCompression, requestMinCompressionSizeBytes: _requestMinCompressionSizeBytes } = input;
return Object.assign(input, {
disableRequestCompression: normalizeProvider(disableRequestCompression),
requestMinCompressionSizeBytes: async () => {
const requestMinCompressionSizeBytes = await normalizeProvider(_requestMinCompressionSizeBytes)();

// The requestMinCompressionSizeBytes should be less than the upper limit for API Gateway
// https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-openapi-minimum-compression-size.html
if (requestMinCompressionSizeBytes < 0 || requestMinCompressionSizeBytes > 10485760) {
throw new RangeError(
"The value for requestMinCompressionSizeBytes must be between 0 and 10485760 inclusive. " +
`The provided value ${requestMinCompressionSizeBytes} is outside this range."`
);
}
// The requestMinCompressionSizeBytes should be less than the upper limit for API Gateway
// https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-openapi-minimum-compression-size.html
if (requestMinCompressionSizeBytes < 0 || requestMinCompressionSizeBytes > 10485760) {
throw new RangeError(
"The value for requestMinCompressionSizeBytes must be between 0 and 10485760 inclusive. " +
`The provided value ${requestMinCompressionSizeBytes} is outside this range."`
);
}

return requestMinCompressionSizeBytes;
},
});
return requestMinCompressionSizeBytes;
},
});
};
17 changes: 17 additions & 0 deletions packages/middleware-endpoint/src/resolveEndpointConfig.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, test as it, vi } from "vitest";

import { resolveEndpointConfig } from "./resolveEndpointConfig";

describe(resolveEndpointConfig.name, () => {
it("maintains object custody", () => {
const input = {
tls: true,
useFipsEndpoint: true,
useDualstackEndpoint: true,
endpointProvider: vi.fn(),
urlParser: vi.fn(),
region: async () => "us-east-1",
};
expect(resolveEndpointConfig(input)).toBe(input);
});
});
11 changes: 5 additions & 6 deletions packages/middleware-endpoint/src/resolveEndpointConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,20 @@ export const resolveEndpointConfig = <T, P extends EndpointParameters = Endpoint
input: T & EndpointInputConfig<P> & PreviouslyResolved<P>
): T & EndpointResolvedConfig<P> => {
const tls = input.tls ?? true;
const { endpoint } = input;
const { endpoint, useDualstackEndpoint, useFipsEndpoint } = input;

const customEndpointProvider =
endpoint != null ? async () => toEndpointV1(await normalizeProvider(endpoint)()) : undefined;

const isCustomEndpoint = !!endpoint;

const resolvedConfig = {
...input,
const resolvedConfig = Object.assign(input, {
endpoint: customEndpointProvider,
tls,
isCustomEndpoint,
useDualstackEndpoint: normalizeProvider(input.useDualstackEndpoint ?? false),
useFipsEndpoint: normalizeProvider(input.useFipsEndpoint ?? false),
} as T & EndpointResolvedConfig<P>;
useDualstackEndpoint: normalizeProvider(useDualstackEndpoint ?? false),
useFipsEndpoint: normalizeProvider(useFipsEndpoint ?? false),
}) as T & EndpointResolvedConfig<P>;

let configuredEndpointPromise: undefined | Promise<string | undefined> = undefined;
resolvedConfig.serviceConfiguredEndpoint = async () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/middleware-retry/src/configurations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ describe(resolveRetryConfig.name, () => {
vi.clearAllMocks();
});

it("maintains object custody", () => {
const input = {
retryMode: "STANDARD",
};
expect(resolveRetryConfig(input)).toBe(input);
});

describe("maxAttempts", () => {
it.each([1, 2, 3])("assigns provided value %s", async (maxAttempts) => {
const output = await resolveRetryConfig({ maxAttempts, retryMode }).maxAttempts();
Expand Down
12 changes: 6 additions & 6 deletions packages/middleware-retry/src/configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,22 @@ export interface RetryResolvedConfig {
* @internal
*/
export const resolveRetryConfig = <T>(input: T & PreviouslyResolved & RetryInputConfig): T & RetryResolvedConfig => {
const { retryStrategy } = input;
const maxAttempts = normalizeProvider(input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
return {
...input,
const { retryStrategy, retryMode: _retryMode, maxAttempts: _maxAttempts } = input;
const maxAttempts = normalizeProvider(_maxAttempts ?? DEFAULT_MAX_ATTEMPTS);

return Object.assign(input, {
maxAttempts,
retryStrategy: async () => {
if (retryStrategy) {
return retryStrategy;
}
const retryMode = await normalizeProvider(input.retryMode)();
const retryMode = await normalizeProvider(_retryMode)();
if (retryMode === RETRY_MODES.ADAPTIVE) {
return new AdaptiveRetryStrategy(maxAttempts);
}
return new StandardRetryStrategy(maxAttempts);
},
};
});
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@ export type HttpHandlerExtensionConfigType<HandlerConfig extends object = {}> =
export const getHttpHandlerExtensionConfiguration = <HandlerConfig extends object = {}>(
runtimeConfig: HttpHandlerExtensionConfigType<HandlerConfig>
) => {
let httpHandler = runtimeConfig.httpHandler!;
return {
setHttpHandler(handler: HttpHandler<HandlerConfig>): void {
httpHandler = handler;
runtimeConfig.httpHandler = handler;
},
httpHandler(): HttpHandler<HandlerConfig> {
return httpHandler;
return runtimeConfig.httpHandler!;
},
updateHttpClientConfig(key: keyof HandlerConfig, value: HandlerConfig[typeof key]): void {
httpHandler.updateHttpClientConfig(key, value);
runtimeConfig.httpHandler?.updateHttpClientConfig(key, value);
},
httpHandlerConfigs(): HandlerConfig {
return httpHandler.httpHandlerConfigs();
return runtimeConfig.httpHandler!.httpHandlerConfigs();
},
};
};
Expand Down
Loading