Skip to content

Commit e917e61

Browse files
authored
chore: enforce singular config object for resolver stack (#1552)
* fix: enforce singular object custody in config resolver stack * separate java changes
1 parent 234b491 commit e917e61

25 files changed

+169
-93
lines changed

.changeset/weak-poems-tease.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@smithy/eventstream-serde-config-resolver": minor
3+
"@smithy/middleware-apply-body-checksum": minor
4+
"@smithy/middleware-compression": minor
5+
"@smithy/middleware-endpoint": minor
6+
"@smithy/middleware-retry": minor
7+
"@smithy/config-resolver": minor
8+
"@smithy/protocol-http": minor
9+
"@smithy/smithy-client": minor
10+
"@smithy/util-stream": minor
11+
"@smithy/types": minor
12+
---
13+
14+
enforce singular config object during client instantiation

packages/config-resolver/src/endpointsConfig/resolveCustomEndpointsConfig.spec.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@ describe(resolveCustomEndpointsConfig.name, () => {
2020

2121
beforeEach(() => {
2222
vi.mocked(normalizeProvider).mockImplementation((input) =>
23-
typeof input === "function" ? input : () => Promise.resolve(input)
23+
typeof input === "function" ? (input as any) : () => Promise.resolve(input)
2424
);
2525
});
2626

2727
afterEach(() => {
2828
vi.clearAllMocks();
2929
});
3030

31+
it("maintains object custody", () => {
32+
const input = { ...mockInput };
33+
expect(resolveCustomEndpointsConfig(input)).toBe(input);
34+
});
35+
3136
describe("tls", () => {
3237
afterEach(() => {
3338
expect(normalizeProvider).toHaveBeenCalledTimes(2);
@@ -44,7 +49,7 @@ describe(resolveCustomEndpointsConfig.name, () => {
4449
});
4550

4651
it("returns true for isCustomEndpoint", () => {
47-
expect(resolveCustomEndpointsConfig(mockInput).isCustomEndpoint).toStrictEqual(true);
52+
expect(resolveCustomEndpointsConfig({ ...mockInput }).isCustomEndpoint).toStrictEqual(true);
4853
});
4954

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

5863
describe("returns normalized endpoint", () => {
59-
afterEach(() => {
60-
expect(normalizeProvider).toHaveBeenCalledTimes(2);
61-
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.endpoint);
62-
expect(normalizeProvider).toHaveBeenNthCalledWith(2, mockInput.useDualstackEndpoint);
63-
});
64-
6564
it("calls urlParser endpoint is of type string", async () => {
6665
const mockEndpointString = "http://localhost/";
6766
const endpoint = await resolveCustomEndpointsConfig({ ...mockInput, endpoint: mockEndpointString }).endpoint();
6867
expect(endpoint).toStrictEqual(mockEndpoint);
6968
expect(mockInput.urlParser).toHaveBeenCalledWith(mockEndpointString);
69+
70+
expect(normalizeProvider).toHaveBeenCalledTimes(2);
71+
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.endpoint);
72+
expect(normalizeProvider).toHaveBeenNthCalledWith(2, mockInput.useDualstackEndpoint);
7073
});
7174

7275
it("passes endpoint to normalize if not string", async () => {
73-
const endpoint = await resolveCustomEndpointsConfig(mockInput).endpoint();
76+
const endpoint = await resolveCustomEndpointsConfig({ ...mockInput }).endpoint();
7477
expect(endpoint).toStrictEqual(mockEndpoint);
7578
expect(mockInput.urlParser).not.toHaveBeenCalled();
79+
80+
expect(normalizeProvider).toHaveBeenCalledTimes(2);
81+
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.endpoint);
82+
expect(normalizeProvider).toHaveBeenNthCalledWith(2, mockInput.useDualstackEndpoint);
7683
});
7784
});
7885
});

packages/config-resolver/src/endpointsConfig/resolveCustomEndpointsConfig.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,11 @@ export interface CustomEndpointsResolvedConfig extends EndpointsResolvedConfig {
3737
export const resolveCustomEndpointsConfig = <T>(
3838
input: T & CustomEndpointsInputConfig & PreviouslyResolved
3939
): T & CustomEndpointsResolvedConfig => {
40-
const { endpoint, urlParser } = input;
41-
return {
42-
...input,
43-
tls: input.tls ?? true,
40+
const { tls, endpoint, urlParser, useDualstackEndpoint } = input;
41+
return Object.assign(input, {
42+
tls: tls ?? true,
4443
endpoint: normalizeProvider(typeof endpoint === "string" ? urlParser(endpoint) : endpoint),
4544
isCustomEndpoint: true,
46-
useDualstackEndpoint: normalizeProvider(input.useDualstackEndpoint ?? false),
47-
};
45+
useDualstackEndpoint: normalizeProvider(useDualstackEndpoint ?? false),
46+
} as CustomEndpointsResolvedConfig);
4847
};

packages/config-resolver/src/endpointsConfig/resolveEndpointsConfig.spec.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,19 @@ describe(resolveEndpointsConfig.name, () => {
2525
beforeEach(() => {
2626
vi.mocked(getEndpointFromRegion).mockResolvedValueOnce(mockEndpoint);
2727
vi.mocked(normalizeProvider).mockImplementation((input) =>
28-
typeof input === "function" ? input : () => Promise.resolve(input)
28+
typeof input === "function" ? (input as any) : () => Promise.resolve(input)
2929
);
3030
});
3131

3232
afterEach(() => {
3333
vi.clearAllMocks();
3434
});
3535

36+
it("maintains object custody", () => {
37+
const input = { ...mockInput };
38+
expect(resolveEndpointsConfig(input)).toBe(input);
39+
});
40+
3641
describe("tls", () => {
3742
afterEach(() => {
3843
expect(normalizeProvider).toHaveBeenNthCalledWith(1, mockInput.useDualstackEndpoint);
@@ -53,7 +58,7 @@ describe(resolveEndpointsConfig.name, () => {
5358
});
5459

5560
it("returns true when endpoint is defined", () => {
56-
expect(resolveEndpointsConfig(mockInput).isCustomEndpoint).toStrictEqual(true);
61+
expect(resolveEndpointsConfig({ ...mockInput }).isCustomEndpoint).toStrictEqual(true);
5762
});
5863

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

9297
it("passes endpoint to normalize if not string", async () => {
93-
const endpoint = await resolveEndpointsConfig(mockInput).endpoint();
98+
const endpoint = await resolveEndpointsConfig({ ...mockInput }).endpoint();
9499
expect(endpoint).toStrictEqual(mockEndpoint);
95100
expect(mockInput.urlParser).not.toHaveBeenCalled();
96101
});

packages/config-resolver/src/endpointsConfig/resolveEndpointsConfig.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,13 @@ export const resolveEndpointsConfig = <T>(
6565
input: T & EndpointsInputConfig & PreviouslyResolved
6666
): T & EndpointsResolvedConfig => {
6767
const useDualstackEndpoint = normalizeProvider(input.useDualstackEndpoint ?? false);
68-
const { endpoint, useFipsEndpoint, urlParser } = input;
69-
return {
70-
...input,
71-
tls: input.tls ?? true,
68+
const { endpoint, useFipsEndpoint, urlParser, tls } = input;
69+
return Object.assign(input, {
70+
tls: tls ?? true,
7271
endpoint: endpoint
7372
? normalizeProvider(typeof endpoint === "string" ? urlParser(endpoint) : endpoint)
7473
: () => getEndpointFromRegion({ ...input, useDualstackEndpoint, useFipsEndpoint }),
7574
isCustomEndpoint: !!endpoint,
7675
useDualstackEndpoint,
77-
};
76+
});
7877
};

packages/config-resolver/src/regionConfig/resolveRegionConfig.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ describe("RegionConfig", () => {
2121
vi.clearAllMocks();
2222
});
2323

24+
it("maintains object custody", () => {
25+
const input = {
26+
region: "us-east-1",
27+
};
28+
expect(resolveRegionConfig(input)).toBe(input);
29+
});
30+
2431
describe("region", () => {
2532
it("return normalized value with real region if passed as a string", async () => {
2633
const resolvedRegionConfig = resolveRegionConfig({ region: mockRegion, useFipsEndpoint: mockUseFipsEndpoint });

packages/config-resolver/src/regionConfig/resolveRegionConfig.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ export const resolveRegionConfig = <T>(input: T & RegionInputConfig & Previously
4545
throw new Error("Region is missing");
4646
}
4747

48-
return {
49-
...input,
48+
return Object.assign(input, {
5049
region: async () => {
5150
if (typeof region === "string") {
5251
return getRealRegion(region);
@@ -61,5 +60,5 @@ export const resolveRegionConfig = <T>(input: T & RegionInputConfig & Previously
6160
}
6261
return typeof useFipsEndpoint !== "function" ? Promise.resolve(!!useFipsEndpoint) : useFipsEndpoint();
6362
},
64-
};
63+
});
6564
};

packages/eventstream-serde-config-resolver/src/EventStreamSerdeConfig.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ describe("resolveEventStreamSerdeConfig", () => {
99
vi.clearAllMocks();
1010
});
1111

12+
it("maintains object custody", () => {
13+
const input = {
14+
eventStreamSerdeProvider: vi.fn(),
15+
};
16+
expect(resolveEventStreamSerdeConfig(input)).toBe(input);
17+
});
18+
1219
it("sets value returned by eventStreamSerdeProvider", () => {
1320
const mockReturn = "mockReturn";
1421
eventStreamSerdeProvider.mockReturnValueOnce(mockReturn);

packages/eventstream-serde-config-resolver/src/EventStreamSerdeConfig.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ interface PreviouslyResolved {
2828
*/
2929
export const resolveEventStreamSerdeConfig = <T>(
3030
input: T & PreviouslyResolved & EventStreamSerdeInputConfig
31-
): T & EventStreamSerdeResolvedConfig => ({
32-
...input,
33-
eventStreamMarshaller: input.eventStreamSerdeProvider(input),
34-
});
31+
): T & EventStreamSerdeResolvedConfig =>
32+
Object.assign(input, {
33+
eventStreamMarshaller: input.eventStreamSerdeProvider(input),
34+
});

packages/middleware-apply-body-checksum/src/index.spec.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import { describe, expect, test as it } from "vitest";
1+
import { describe, expect, test as it, vi } from "vitest";
22

3-
import { applyMd5BodyChecksumMiddleware } from "./index";
3+
import { applyMd5BodyChecksumMiddleware, resolveMd5BodyChecksumConfig } from "./index";
44

55
describe("middleware-apply-body-checksum package exports", () => {
6+
it("maintains object custody", () => {
7+
const input = {
8+
md5: vi.fn(),
9+
base64Encoder: vi.fn(),
10+
streamHasher: vi.fn(),
11+
};
12+
expect(resolveMd5BodyChecksumConfig(input)).toBe(input);
13+
});
14+
615
it("applyMd5BodyChecksumMiddleware", () => {
716
expect(typeof applyMd5BodyChecksumMiddleware).toBe("function");
817
});

packages/middleware-compression/src/resolveCompressionConfig.spec.ts

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ describe(resolveCompressionConfig.name, () => {
99
requestMinCompressionSizeBytes: 0,
1010
};
1111

12+
it("maintains object custody", () => {
13+
const input = {
14+
disableRequestCompression: false,
15+
requestMinCompressionSizeBytes: 10_000,
16+
};
17+
expect(resolveCompressionConfig(input)).toBe(input);
18+
});
19+
1220
it("should throw an error if requestMinCompressionSizeBytes is less than 0", async () => {
1321
const requestMinCompressionSizeBytes = -1;
1422
const resolvedConfig = resolveCompressionConfig({ ...mockConfig, requestMinCompressionSizeBytes });

packages/middleware-compression/src/resolveCompressionConfig.ts

+18-16
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@ import { CompressionInputConfig, CompressionResolvedConfig } from "./configurati
77
*/
88
export const resolveCompressionConfig = <T>(
99
input: T & Required<CompressionInputConfig>
10-
): T & CompressionResolvedConfig => ({
11-
...input,
12-
disableRequestCompression: normalizeProvider(input.disableRequestCompression),
13-
requestMinCompressionSizeBytes: async () => {
14-
const requestMinCompressionSizeBytes = await normalizeProvider(input.requestMinCompressionSizeBytes)();
10+
): T & CompressionResolvedConfig => {
11+
const { disableRequestCompression, requestMinCompressionSizeBytes: _requestMinCompressionSizeBytes } = input;
12+
return Object.assign(input, {
13+
disableRequestCompression: normalizeProvider(disableRequestCompression),
14+
requestMinCompressionSizeBytes: async () => {
15+
const requestMinCompressionSizeBytes = await normalizeProvider(_requestMinCompressionSizeBytes)();
1516

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

25-
return requestMinCompressionSizeBytes;
26-
},
27-
});
26+
return requestMinCompressionSizeBytes;
27+
},
28+
});
29+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, test as it, vi } from "vitest";
2+
3+
import { resolveEndpointConfig } from "./resolveEndpointConfig";
4+
5+
describe(resolveEndpointConfig.name, () => {
6+
it("maintains object custody", () => {
7+
const input = {
8+
tls: true,
9+
useFipsEndpoint: true,
10+
useDualstackEndpoint: true,
11+
endpointProvider: vi.fn(),
12+
urlParser: vi.fn(),
13+
region: async () => "us-east-1",
14+
};
15+
expect(resolveEndpointConfig(input)).toBe(input);
16+
});
17+
});

packages/middleware-endpoint/src/resolveEndpointConfig.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,20 @@ export const resolveEndpointConfig = <T, P extends EndpointParameters = Endpoint
121121
input: T & EndpointInputConfig<P> & PreviouslyResolved<P>
122122
): T & EndpointResolvedConfig<P> => {
123123
const tls = input.tls ?? true;
124-
const { endpoint } = input;
124+
const { endpoint, useDualstackEndpoint, useFipsEndpoint } = input;
125125

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

129129
const isCustomEndpoint = !!endpoint;
130130

131-
const resolvedConfig = {
132-
...input,
131+
const resolvedConfig = Object.assign(input, {
133132
endpoint: customEndpointProvider,
134133
tls,
135134
isCustomEndpoint,
136-
useDualstackEndpoint: normalizeProvider(input.useDualstackEndpoint ?? false),
137-
useFipsEndpoint: normalizeProvider(input.useFipsEndpoint ?? false),
138-
} as T & EndpointResolvedConfig<P>;
135+
useDualstackEndpoint: normalizeProvider(useDualstackEndpoint ?? false),
136+
useFipsEndpoint: normalizeProvider(useFipsEndpoint ?? false),
137+
}) as T & EndpointResolvedConfig<P>;
139138

140139
let configuredEndpointPromise: undefined | Promise<string | undefined> = undefined;
141140
resolvedConfig.serviceConfiguredEndpoint = async () => {

packages/middleware-retry/src/configurations.spec.ts

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ describe(resolveRetryConfig.name, () => {
2626
vi.clearAllMocks();
2727
});
2828

29+
it("maintains object custody", () => {
30+
const input = {
31+
retryMode: "STANDARD",
32+
};
33+
expect(resolveRetryConfig(input)).toBe(input);
34+
});
35+
2936
describe("maxAttempts", () => {
3037
it.each([1, 2, 3])("assigns provided value %s", async (maxAttempts) => {
3138
const output = await resolveRetryConfig({ maxAttempts, retryMode }).maxAttempts();

packages/middleware-retry/src/configurations.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,22 @@ export interface RetryResolvedConfig {
8686
* @internal
8787
*/
8888
export const resolveRetryConfig = <T>(input: T & PreviouslyResolved & RetryInputConfig): T & RetryResolvedConfig => {
89-
const { retryStrategy } = input;
90-
const maxAttempts = normalizeProvider(input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
91-
return {
92-
...input,
89+
const { retryStrategy, retryMode: _retryMode, maxAttempts: _maxAttempts } = input;
90+
const maxAttempts = normalizeProvider(_maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
91+
92+
return Object.assign(input, {
9393
maxAttempts,
9494
retryStrategy: async () => {
9595
if (retryStrategy) {
9696
return retryStrategy;
9797
}
98-
const retryMode = await normalizeProvider(input.retryMode)();
98+
const retryMode = await normalizeProvider(_retryMode)();
9999
if (retryMode === RETRY_MODES.ADAPTIVE) {
100100
return new AdaptiveRetryStrategy(maxAttempts);
101101
}
102102
return new StandardRetryStrategy(maxAttempts);
103103
},
104-
};
104+
});
105105
};
106106

107107
/**

packages/protocol-http/src/extensions/httpExtensionConfiguration.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,18 @@ export type HttpHandlerExtensionConfigType<HandlerConfig extends object = {}> =
2525
export const getHttpHandlerExtensionConfiguration = <HandlerConfig extends object = {}>(
2626
runtimeConfig: HttpHandlerExtensionConfigType<HandlerConfig>
2727
) => {
28-
let httpHandler = runtimeConfig.httpHandler!;
2928
return {
3029
setHttpHandler(handler: HttpHandler<HandlerConfig>): void {
31-
httpHandler = handler;
30+
runtimeConfig.httpHandler = handler;
3231
},
3332
httpHandler(): HttpHandler<HandlerConfig> {
34-
return httpHandler;
33+
return runtimeConfig.httpHandler!;
3534
},
3635
updateHttpClientConfig(key: keyof HandlerConfig, value: HandlerConfig[typeof key]): void {
37-
httpHandler.updateHttpClientConfig(key, value);
36+
runtimeConfig.httpHandler?.updateHttpClientConfig(key, value);
3837
},
3938
httpHandlerConfigs(): HandlerConfig {
40-
return httpHandler.httpHandlerConfigs();
39+
return runtimeConfig.httpHandler!.httpHandlerConfigs();
4140
},
4241
};
4342
};

0 commit comments

Comments
 (0)