Skip to content

Commit 43f3e1e

Browse files
authored
feat: encoders accept strings (#1176)
* feat: encoders accept strings * add test cases * typecheck in Encoder functions
1 parent 49640d6 commit 43f3e1e

18 files changed

+177
-35
lines changed

.changeset/fluffy-deers-doubt.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@smithy/middleware-serde": minor
3+
"@smithy/util-base64": minor
4+
"@smithy/util-utf8": minor
5+
"@smithy/types": minor
6+
---
7+
8+
encoders allow string inputs

packages/middleware-serde/src/deserializerMiddleware.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { EndpointBearer, SerdeFunctions } from "@smithy/types";
2+
13
import { deserializerMiddleware } from "./deserializerMiddleware";
24

35
describe("deserializerMiddleware", () => {
@@ -11,7 +13,7 @@ describe("deserializerMiddleware", () => {
1113
hostname: "hostname",
1214
path: "path",
1315
}),
14-
};
16+
} as EndpointBearer & SerdeFunctions;
1517

1618
const mockArgs = {
1719
input: {

packages/middleware-serde/src/deserializerMiddleware.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import {
55
DeserializeMiddleware,
66
HandlerExecutionContext,
77
ResponseDeserializer,
8+
SerdeFunctions,
89
} from "@smithy/types";
910

10-
export const deserializerMiddleware = <Input extends object, Output extends object, RuntimeUtils = any>(
11-
options: RuntimeUtils,
12-
deserializer: ResponseDeserializer<any, any, RuntimeUtils>
11+
/**
12+
* @internal
13+
*
14+
* 3rd type parameter is deprecated and unused.
15+
*/
16+
export const deserializerMiddleware = <Input extends object, Output extends object, _ = any>(
17+
options: SerdeFunctions,
18+
deserializer: ResponseDeserializer<any, any, SerdeFunctions>
1319
): DeserializeMiddleware<Input, Output> => (
1420
next: DeserializeHandler<Input, Output>,
1521
context: HandlerExecutionContext

packages/middleware-serde/src/serdePlugin.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Provider,
99
RequestSerializer,
1010
ResponseDeserializer,
11+
SerdeFunctions,
1112
SerializeHandlerOptions,
1213
UrlParser,
1314
} from "@smithy/types";
@@ -39,14 +40,19 @@ export type V1OrV2Endpoint = {
3940
endpoint?: Provider<Endpoint>;
4041
};
4142

42-
export function getSerdePlugin<InputType extends object, SerDeContext, OutputType extends MetadataBearer>(
43-
config: V1OrV2Endpoint,
44-
serializer: RequestSerializer<any, SerDeContext & EndpointBearer>,
45-
deserializer: ResponseDeserializer<OutputType, any, SerDeContext>
43+
/**
44+
* @internal
45+
*
46+
* Note: 2nd type parameter is deprecated and unused.
47+
*/
48+
export function getSerdePlugin<InputType extends object, _, OutputType extends MetadataBearer>(
49+
config: V1OrV2Endpoint & SerdeFunctions,
50+
serializer: RequestSerializer<any, SerdeFunctions & EndpointBearer>,
51+
deserializer: ResponseDeserializer<OutputType, any, SerdeFunctions>
4652
): Pluggable<InputType, OutputType> {
4753
return {
4854
applyToStack: (commandStack: MiddlewareStack<InputType, OutputType>) => {
49-
commandStack.add(deserializerMiddleware(config as SerDeContext, deserializer), deserializerMiddlewareOption);
55+
commandStack.add(deserializerMiddleware(config, deserializer), deserializerMiddlewareOption);
5056
commandStack.add(serializerMiddleware(config, serializer), serializerMiddlewareOption);
5157
},
5258
};

packages/middleware-serde/src/serializerMiddleware.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EndpointBearer } from "@smithy/types";
1+
import { EndpointBearer, SerdeFunctions } from "@smithy/types";
22

33
import { serializerMiddleware } from "./serializerMiddleware";
44

@@ -13,7 +13,7 @@ describe("serializerMiddleware", () => {
1313
hostname: "hostname",
1414
path: "path",
1515
}),
16-
};
16+
} as EndpointBearer & SerdeFunctions;
1717

1818
const mockRequest = {
1919
method: "GET",

packages/middleware-serde/src/serializerMiddleware.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
EndpointBearer,
33
HandlerExecutionContext,
44
RequestSerializer,
5+
SerdeFunctions,
56
SerializeHandler,
67
SerializeHandlerArguments,
78
SerializeHandlerOutput,
@@ -10,9 +11,14 @@ import {
1011

1112
import type { V1OrV2Endpoint } from "./serdePlugin";
1213

13-
export const serializerMiddleware = <Input extends object, Output extends object, RuntimeUtils extends EndpointBearer>(
14-
options: V1OrV2Endpoint,
15-
serializer: RequestSerializer<any, RuntimeUtils>
14+
/**
15+
* @internal
16+
*
17+
* Note: 3rd type parameter is deprecated and unused.
18+
*/
19+
export const serializerMiddleware = <Input extends object, Output extends object, _>(
20+
options: V1OrV2Endpoint & SerdeFunctions,
21+
serializer: RequestSerializer<any, SerdeFunctions & EndpointBearer>
1622
): SerializeMiddleware<Input, Output> => (
1723
next: SerializeHandler<Input, Output>,
1824
context: HandlerExecutionContext
@@ -28,7 +34,7 @@ export const serializerMiddleware = <Input extends object, Output extends object
2834
throw new Error("No valid endpoint provider available.");
2935
}
3036

31-
const request = await serializer(args.input, { ...options, endpoint } as RuntimeUtils);
37+
const request = await serializer(args.input, { ...options, endpoint });
3238

3339
return next({
3440
...args,

packages/types/src/serde.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,22 @@ export interface StreamCollector {
3131
*
3232
* Request and Response serde util functions and settings for AWS services
3333
*/
34-
export interface SerdeContext extends EndpointBearer {
34+
export interface SerdeContext extends SerdeFunctions, EndpointBearer {
35+
requestHandler: RequestHandler<any, any>;
36+
disableHostPrefix: boolean;
37+
}
38+
39+
/**
40+
* @public
41+
*
42+
* Serde functions from the client config.
43+
*/
44+
export interface SerdeFunctions {
3545
base64Encoder: Encoder;
3646
base64Decoder: Decoder;
3747
utf8Encoder: Encoder;
3848
utf8Decoder: Decoder;
3949
streamCollector: StreamCollector;
40-
requestHandler: RequestHandler<any, any>;
41-
disableHostPrefix: boolean;
4250
}
4351

4452
/**

packages/types/src/util.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,23 @@ export type Exact<Type1, Type2> = [Type1] extends [Type2] ? ([Type2] extends [Ty
1212
/**
1313
* @public
1414
*
15-
* A function that, given a TypedArray of bytes, can produce a string
16-
* representation thereof.
15+
* A function that, given a Uint8Array of bytes, can produce a string
16+
* representation thereof. The function may optionally attempt to
17+
* convert other input types to Uint8Array before encoding.
1718
*
1819
* @example An encoder function that converts bytes to hexadecimal
19-
* representation would return `'deadbeef'` when given
20-
* `new Uint8Array([0xde, 0xad, 0xbe, 0xef])`.
20+
* representation would return `'hello'` when given
21+
* `new Uint8Array([104, 101, 108, 108, 111])`.
2122
*/
2223
export interface Encoder {
23-
(input: Uint8Array): string;
24+
/**
25+
* Caution: the `any` type on the input is for backwards compatibility.
26+
* Runtime support is limited to Uint8Array and string by default.
27+
*
28+
* You may choose to support more encoder input types if overriding the default
29+
* implementations.
30+
*/
31+
(input: Uint8Array | string | any): string;
2432
}
2533

2634
/**
@@ -30,8 +38,8 @@ export interface Encoder {
3038
* string.
3139
*
3240
* @example A decoder function that converts bytes to hexadecimal
33-
* representation would return `new Uint8Array([0xde, 0xad, 0xbe, 0xef])` when
34-
* given the string `'deadbeef'`.
41+
* representation would return `new Uint8Array([104, 101, 108, 108, 111])` when
42+
* given the string `'hello'`.
3543
*/
3644
export interface Decoder {
3745
(input: string): Uint8Array;

packages/util-base64/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"license": "Apache-2.0",
2424
"dependencies": {
2525
"@smithy/util-buffer-from": "workspace:^",
26+
"@smithy/util-utf8": "workspace:^",
2627
"tslib": "^2.5.0"
2728
},
2829
"devDependencies": {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
/**
22
* @jest-environment jsdom
33
*/
4+
import type { Encoder } from "@smithy/types";
5+
46
import testCases from "./__mocks__/testCases.json";
57
import { toBase64 } from "./toBase64.browser";
68

79
describe(toBase64.name, () => {
810
it.each(testCases as Array<[string, string, number[]]>)("%s", (desc, encoded, decoded) => {
911
expect(toBase64(new Uint8Array(decoded))).toEqual(encoded);
1012
});
13+
14+
it("also converts strings", () => {
15+
expect(toBase64("hello")).toEqual("aGVsbG8=");
16+
});
17+
18+
it("throws on non-string non-Uint8Array", () => {
19+
expect(() => (toBase64 as Encoder)(new Date())).toThrow();
20+
expect(() => (toBase64 as Encoder)({})).toThrow();
21+
});
1122
});

packages/util-base64/src/toBase64.browser.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1+
import { fromUtf8 } from "@smithy/util-utf8";
2+
13
import { alphabetByValue, bitsPerByte, bitsPerLetter, maxLetterValue } from "./constants.browser";
4+
25
/**
3-
* Converts a Uint8Array of binary data to a base-64 encoded string.
6+
* Converts a Uint8Array of binary data or a utf-8 string to a base-64 encoded string.
47
*
5-
* @param input The binary data to encode
8+
* @param _input - the binary data or string to encode.
9+
* @returns base64 string.
610
*
711
* @see https://tools.ietf.org/html/rfc4648#section-4
812
*/
9-
export function toBase64(input: Uint8Array): string {
13+
export function toBase64(_input: Uint8Array | string): string {
14+
let input: Uint8Array;
15+
if (typeof _input === "string") {
16+
input = fromUtf8(_input);
17+
} else {
18+
input = _input as Uint8Array;
19+
}
20+
if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") {
21+
throw new Error("@smithy/util-base64: toBase64 encoder function only accepts string | Uint8Array.");
22+
}
1023
let str = "";
1124
for (let i = 0; i < input.length; i += 3) {
1225
let bits = 0;

packages/util-base64/src/toBase64.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Encoder } from "@smithy/types";
2+
13
import testCases from "./__mocks__/testCases.json";
24
import { toBase64 } from "./toBase64";
35

@@ -9,4 +11,13 @@ describe(toBase64.name, () => {
911
it("should throw when given a number", () => {
1012
expect(() => toBase64(0xdeadbeefface as any)).toThrow();
1113
});
14+
15+
it("also converts strings", () => {
16+
expect(toBase64("hello")).toEqual("aGVsbG8=");
17+
});
18+
19+
it("throws on non-string non-Uint8Array", () => {
20+
expect(() => (toBase64 as Encoder)(new Date())).toThrow();
21+
expect(() => (toBase64 as Encoder)({})).toThrow();
22+
});
1223
});

packages/util-base64/src/toBase64.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import { fromArrayBuffer } from "@smithy/util-buffer-from";
2+
import { fromUtf8 } from "@smithy/util-utf8";
23

34
/**
4-
* Converts a Uint8Array of binary data to a base-64 encoded string using
5+
* Converts a Uint8Array of binary data or a utf-8 string to a base-64 encoded string using
56
* Node.JS's `buffer` module.
67
*
7-
* @param input The binary data to encode
8+
* @param _input - the binary data or string to encode.
9+
* @returns base64 string.
810
*/
9-
export const toBase64 = (input: Uint8Array): string =>
10-
fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("base64");
11+
export const toBase64 = (_input: Uint8Array | string): string => {
12+
let input: Uint8Array;
13+
if (typeof _input === "string") {
14+
input = fromUtf8(_input);
15+
} else {
16+
input = _input as Uint8Array;
17+
}
18+
if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") {
19+
throw new Error("@smithy/util-base64: toBase64 encoder function only accepts string | Uint8Array.");
20+
}
21+
return fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("base64");
22+
};

packages/util-utf8/src/toUtf8.browser.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* @jest-environment jsdom
33
*/
4+
import type { Encoder } from "@smithy/types";
5+
46
import { toUtf8 } from "./toUtf8.browser";
57

68
declare const global: any;
@@ -13,4 +15,13 @@ describe("toUtf8", () => {
1315

1416
expect(toUtf8(new Uint8Array(0))).toBe(expected);
1517
});
18+
19+
it("passes through strings", () => {
20+
expect(toUtf8("hello")).toEqual("hello");
21+
});
22+
23+
it("throws on non-string non-Uint8Array", () => {
24+
expect(() => (toUtf8 as Encoder)(new Date())).toThrow();
25+
expect(() => (toUtf8 as Encoder)({})).toThrow();
26+
});
1627
});
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
1-
export const toUtf8 = (input: Uint8Array): string => new TextDecoder("utf-8").decode(input);
1+
/**
2+
*
3+
* This does not convert non-utf8 strings to utf8, it only passes through strings if
4+
* a string is received instead of a Uint8Array.
5+
*
6+
*/
7+
export const toUtf8 = (input: Uint8Array | string): string => {
8+
if (typeof input === "string") {
9+
return input;
10+
}
11+
if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") {
12+
throw new Error("@smithy/util-utf8: toUtf8 encoder function only accepts string | Uint8Array.");
13+
}
14+
return new TextDecoder("utf-8").decode(input);
15+
};

packages/util-utf8/src/toUtf8.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Encoder } from "@smithy/types";
2+
13
import { toUtf8 } from "./toUtf8";
24

35
const utf8StringsToByteArrays: Record<string, Uint8Array> = {
@@ -144,4 +146,13 @@ describe("toUtf8", () => {
144146
it("should throw when given a number", () => {
145147
expect(() => toUtf8(255 as any)).toThrow();
146148
});
149+
150+
it("passes through strings", () => {
151+
expect(toUtf8("hello")).toEqual("hello");
152+
});
153+
154+
it("throws on non-string non-Uint8Array", () => {
155+
expect(() => (toUtf8 as Encoder)(new Date())).toThrow();
156+
expect(() => (toUtf8 as Encoder)({})).toThrow();
157+
});
147158
});

packages/util-utf8/src/toUtf8.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
import { fromArrayBuffer } from "@smithy/util-buffer-from";
22

3-
export const toUtf8 = (input: Uint8Array): string =>
4-
fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("utf8");
3+
/**
4+
*
5+
* This does not convert non-utf8 strings to utf8, it only passes through strings if
6+
* a string is received instead of a Uint8Array.
7+
*
8+
*/
9+
export const toUtf8 = (input: Uint8Array | string): string => {
10+
if (typeof input === "string") {
11+
return input;
12+
}
13+
if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") {
14+
throw new Error("@smithy/util-utf8: toUtf8 encoder function only accepts string | Uint8Array.");
15+
}
16+
return fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("utf8");
17+
};

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,6 +2554,7 @@ __metadata:
25542554
resolution: "@smithy/util-base64@workspace:packages/util-base64"
25552555
dependencies:
25562556
"@smithy/util-buffer-from": "workspace:^"
2557+
"@smithy/util-utf8": "workspace:^"
25572558
"@tsconfig/recommended": 1.0.1
25582559
"@types/node": ^14.14.31
25592560
concurrently: 7.0.0

0 commit comments

Comments
 (0)