Skip to content

Commit

Permalink
fix(types): add client config interface test with s3 example (#4156)
Browse files Browse the repository at this point in the history
* fix(types): add client config interface test with s3 example

* fix(types): decouple V1orV2Endpoint type from EndpointBearer type

* fix(lib-storage): type fix, allow undefined endpoint

* fix(types): code reorganization in client-api-test
  • Loading branch information
kuhe authored Nov 10, 2022
1 parent 302e5b2 commit 7811cd9
Show file tree
Hide file tree
Showing 19 changed files with 384 additions and 16 deletions.
2 changes: 1 addition & 1 deletion lib/lib-storage/src/Upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class Upload extends EventEmitter {

const resolved = await Promise.all([this.client.send(new PutObjectCommand(params)), clientConfig?.endpoint?.()]);
const putResult = resolved[0];
let endpoint: Endpoint = resolved[1];
let endpoint: Endpoint | undefined = resolved[1];

if (!endpoint) {
endpoint = toEndpointV1(
Expand Down
13 changes: 10 additions & 3 deletions packages/middleware-endpoint/src/resolveEndpointConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,17 @@ interface PreviouslyResolved<T extends EndpointParameters = EndpointParameters>
*/
export interface EndpointResolvedConfig<T extends EndpointParameters = EndpointParameters> {
/**
* Resolved value for input {@link EndpointsInputConfig.endpoint}
* @deprecated Use {@link EndpointResolvedConfig.endpointProvider} instead
* Custom endpoint provided by the user.
* This is normalized to a single interface from the various acceptable types.
* This field will be undefined if a custom endpoint is not provided.
*
* As of endpoints 2.0, this config method can not be used to resolve
* the endpoint for a service and region.
*
* @see https://github.com/aws/aws-sdk-js-v3/issues/4122
* @deprecated Use {@link EndpointResolvedConfig.endpointProvider} instead.
*/
endpoint: Provider<Endpoint>;
endpoint?: Provider<Endpoint>;

endpointProvider: (params: T, context?: { logger?: Logger }) => EndpointV2;

Expand Down
19 changes: 10 additions & 9 deletions packages/middleware-serde/src/serdePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
MetadataBearer,
MiddlewareStack,
Pluggable,
Provider,
RequestSerializer,
ResponseDeserializer,
SerializeHandlerOptions,
Expand All @@ -30,22 +31,22 @@ export const serializerMiddlewareOption: SerializeHandlerOptions = {

// Type the modifies the EndpointBearer to make it compatible with Endpoints 2.0 change.
// Must be removed after all clients has been onboard the Endpoints 2.0
export type V1OrV2Endpoint<T extends EndpointBearer> = T & {
export type V1OrV2Endpoint = {
// for v2
urlParser?: UrlParser;

// for v1
endpoint?: Provider<Endpoint>;
};

export function getSerdePlugin<
InputType extends object,
SerDeContext extends EndpointBearer,
OutputType extends MetadataBearer
>(
config: V1OrV2Endpoint<SerDeContext>,
serializer: RequestSerializer<any, SerDeContext>,
export function getSerdePlugin<InputType extends object, SerDeContext, OutputType extends MetadataBearer>(
config: V1OrV2Endpoint,
serializer: RequestSerializer<any, SerDeContext & EndpointBearer>,
deserializer: ResponseDeserializer<OutputType, any, SerDeContext>
): Pluggable<InputType, OutputType> {
return {
applyToStack: (commandStack: MiddlewareStack<InputType, OutputType>) => {
commandStack.add(deserializerMiddleware(config, deserializer), deserializerMiddlewareOption);
commandStack.add(deserializerMiddleware(config as SerDeContext, deserializer), deserializerMiddlewareOption);
commandStack.add(serializerMiddleware(config, serializer), serializerMiddlewareOption);
},
};
Expand Down
6 changes: 3 additions & 3 deletions packages/middleware-serde/src/serializerMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ import type { V1OrV2Endpoint } from "./serdePlugin";

export const serializerMiddleware =
<Input extends object, Output extends object, RuntimeUtils extends EndpointBearer>(
options: V1OrV2Endpoint<RuntimeUtils>,
options: V1OrV2Endpoint,
serializer: RequestSerializer<any, RuntimeUtils>
): SerializeMiddleware<Input, Output> =>
(next: SerializeHandler<Input, Output>, context: HandlerExecutionContext): SerializeHandler<Input, Output> =>
async (args: SerializeHandlerArguments<Input>): Promise<SerializeHandlerOutput<Output>> => {
const endpoint =
context.endpointV2?.url && options.urlParser
? async () => options.urlParser!(context.endpointV2!.url as URL)
: options.endpoint;
: options.endpoint!;

if (!endpoint) {
throw new Error("No valid endpoint provider available.");
}

const request = await serializer(args.input, { ...options, endpoint });
const request = await serializer(args.input, { ...options, endpoint } as RuntimeUtils);

return next({
...args,
Expand Down
5 changes: 5 additions & 0 deletions private/client-api-test/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const base = require("../../jest.config.base.js");

module.exports = {
...base,
};
61 changes: 61 additions & 0 deletions private/client-api-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@aws-sdk/client-api-test",
"description": "Test suite for client interface stability",
"version": "3.0.0",
"scripts": {
"build": "concurrently 'yarn:build:cjs' 'yarn:build:es' 'yarn:build:types'",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:docs": "typedoc",
"build:es": "tsc -p tsconfig.es.json",
"build:include:deps": "lerna run --scope $npm_package_name --include-dependencies build",
"build:types": "tsc -p tsconfig.types.json",
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
"test": "jest --coverage --passWithNoTests"
},
"main": "./dist-cjs/index.js",
"types": "./dist-types/index.d.ts",
"module": "./dist-es/index.js",
"sideEffects": false,
"dependencies": {
"@aws-sdk/client-s3": "*",
"tslib": "^2.3.1"
},
"devDependencies": {
"@tsconfig/node14": "1.0.3",
"@types/node": "^12.7.5",
"concurrently": "7.0.0",
"downlevel-dts": "0.10.1",
"typedoc": "0.19.2",
"typescript": "~4.6.2"
},
"overrides": {
"typedoc": {
"typescript": "~4.6.2"
}
},
"engines": {
"node": ">=14.0.0"
},
"typesVersions": {
"<4.0": {
"dist-types/*": [
"dist-types/ts3.4/*"
]
}
},
"files": [
"dist-*"
],
"author": {
"name": "AWS SDK for JavaScript Team",
"url": "https://aws.amazon.com/javascript/"
},
"license": "Apache-2.0",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-js-v3.git",
"directory": "private/client-api-test"
}
}
10 changes: 10 additions & 0 deletions private/client-api-test/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# @aws-sdk/client-api-test

This is not a runtime or published package.

This is a test spec.

The purpose of this package is to stabilize the `@aws-sdk/client-*` interface against changes.

If tests in this package fail, the author should either fix their changes such that the API contract
is maintained, or appropriately announce and safely deprecate the interfaces affected by incoming changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Pattern for testing the stability of a Client interface.
*/
export interface ClientInterfaceTest<Client> {
/**
* Assert that some resolved config fields can be set to undefined.
*/
optionalConfigFieldsCanBeVoided(): void;
/**
* Create a test that initializes a client
* with the minimal number of user-supplied values. This is
* usually 0.
*
* This method is also a compilation test.
*/
initializeWithMinimalConfiguration(): Client;
/**
* Create a test that initializes a client with all config fields supplied
* by the user.
*
* This method is also a compilation test.
*/
initializeWithMaximalConfiguration(): Client;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* The status of a config field after passing through constructor
* resolvers.
*/
export type FIELD_INIT_TYPE = "resolvedByConfigResolver" | "resolvedOnlyIfProvided" | "neverResolved";
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ClientS3InterfaceTest } from "./ClientS3InterfaceTest";
import { RESOLVED_FIELDS } from "./RESOLVED_FIELDS";

const Subject = ClientS3InterfaceTest;

describe("Client config interface should be stable", () => {
describe(ClientS3InterfaceTest.name, () => {
describe("initialization with minimal configuration", () => {
const client = new Subject().initializeWithMinimalConfiguration();
for (const [configType, fields] of Object.entries(RESOLVED_FIELDS)) {
for (const field of fields) {
if (configType === "resolvedByConfigResolver") {
it(`should resolve the field [${field}] after minimal client init`, () => {
expect(client.config[field as keyof typeof client.config]).toBeDefined();
});
} else {
it(`should not resolve the field [${field}] after minimal client init`, () => {
expect(client.config[field as keyof typeof client.config]).not.toBeDefined();
});
}
}
}
});
describe("initialization with maximal configuration", () => {
const client = new Subject().initializeWithMaximalConfiguration();
for (const [configType, fields] of Object.entries(RESOLVED_FIELDS)) {
for (const field of fields) {
if (configType === "resolvedByConfigResolver" || configType === "resolvedOnlyIfProvided") {
it(`should resolve the field [${field}] after maximally configured client init`, () => {
expect(client.config[field as keyof typeof client.config]).toBeDefined();
});
} else {
it(`should not resolve the field [${field}] after maximally configured client init`, () => {
expect(client.config[field as keyof typeof client.config]).not.toBeDefined();
});
}
}
}
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { S3Client, S3ClientResolvedConfig } from "@aws-sdk/client-s3";

import { ClientInterfaceTest } from "../ClientInterfaceTest";
import { initializeWithMaximalConfiguration } from "./impl/initializeWithMaximalConfiguration";
import { initializeWithMinimalConfiguration } from "./impl/initializeWithMinimalConfiguration";

export class ClientS3InterfaceTest implements ClientInterfaceTest<S3Client> {
optionalConfigFieldsCanBeVoided(): void {
const s3 = new S3Client({});
const resolvedConfig: S3ClientResolvedConfig = s3.config;
/**
* Endpoint is no longer guaranteed as of endpoints 2.0 (rulesets).
* @see https://github.com/aws/aws-sdk-js-v3/issues/4122
*/
resolvedConfig.endpoint = void 0;
resolvedConfig.isCustomEndpoint = void 0;
resolvedConfig.customUserAgent = void 0;
}
initializeWithMinimalConfiguration(): S3Client {
return initializeWithMinimalConfiguration();
}
initializeWithMaximalConfiguration(): S3Client {
return initializeWithMaximalConfiguration();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { S3ClientResolvedConfig } from "@aws-sdk/client-s3";

import { FIELD_INIT_TYPE } from "../FIELD_INIT_TYPE";

export const RESOLVED_FIELDS: Record<FIELD_INIT_TYPE, (keyof S3ClientResolvedConfig)[]> = {
resolvedByConfigResolver: [
"requestHandler",
"apiVersion",
"sha256",
"urlParser",
"bodyLengthChecker",
"streamCollector",
"base64Decoder",
"base64Encoder",
"utf8Decoder",
"utf8Encoder",
"runtime",
"disableHostPrefix",
"maxAttempts",
"retryMode",
"logger",
"useDualstackEndpoint",
"useFipsEndpoint",
"serviceId",
"region",
"credentialDefaultProvider",
"signingEscapePath",
"useArnRegion",
"defaultUserAgentProvider",
"streamHasher",
"md5",
"sha1",
"getAwsChunkedEncodingStream",
"eventStreamSerdeProvider",
"defaultsMode",
"sdkStreamMixin",
"endpointProvider",
"tls",
"isCustomEndpoint",
"retryStrategy",
"credentials",
"signer",
"systemClockOffset",
"forcePathStyle",
"useAccelerateEndpoint",
"disableMultiregionAccessPoints",
"eventStreamMarshaller",
"defaultSigningName",
"useGlobalEndpoint",
],
resolvedOnlyIfProvided: ["customUserAgent", "endpoint"],
neverResolved: [],
};
Loading

0 comments on commit 7811cd9

Please sign in to comment.