From 43f3e1e20925c482d35e2838904643f2321cb8d0 Mon Sep 17 00:00:00 2001 From: George Fu Date: Thu, 7 Mar 2024 10:57:21 -0500 Subject: [PATCH] feat: encoders accept strings (#1176) * feat: encoders accept strings * add test cases * typecheck in Encoder functions --- .changeset/fluffy-deers-doubt.md | 8 +++++++ .../src/deserializerMiddleware.spec.ts | 4 +++- .../src/deserializerMiddleware.ts | 12 +++++++--- packages/middleware-serde/src/serdePlugin.ts | 16 +++++++++----- .../src/serializerMiddleware.spec.ts | 4 ++-- .../src/serializerMiddleware.ts | 14 ++++++++---- packages/types/src/serde.ts | 14 +++++++++--- packages/types/src/util.ts | 22 +++++++++++++------ packages/util-base64/package.json | 1 + .../util-base64/src/toBase64.browser.spec.ts | 11 ++++++++++ packages/util-base64/src/toBase64.browser.ts | 19 +++++++++++++--- packages/util-base64/src/toBase64.spec.ts | 11 ++++++++++ packages/util-base64/src/toBase64.ts | 20 +++++++++++++---- packages/util-utf8/src/toUtf8.browser.spec.ts | 11 ++++++++++ packages/util-utf8/src/toUtf8.browser.ts | 16 +++++++++++++- packages/util-utf8/src/toUtf8.spec.ts | 11 ++++++++++ packages/util-utf8/src/toUtf8.ts | 17 ++++++++++++-- yarn.lock | 1 + 18 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 .changeset/fluffy-deers-doubt.md diff --git a/.changeset/fluffy-deers-doubt.md b/.changeset/fluffy-deers-doubt.md new file mode 100644 index 00000000000..a26eca305e9 --- /dev/null +++ b/.changeset/fluffy-deers-doubt.md @@ -0,0 +1,8 @@ +--- +"@smithy/middleware-serde": minor +"@smithy/util-base64": minor +"@smithy/util-utf8": minor +"@smithy/types": minor +--- + +encoders allow string inputs diff --git a/packages/middleware-serde/src/deserializerMiddleware.spec.ts b/packages/middleware-serde/src/deserializerMiddleware.spec.ts index e09717c9b00..2e870e02e69 100644 --- a/packages/middleware-serde/src/deserializerMiddleware.spec.ts +++ b/packages/middleware-serde/src/deserializerMiddleware.spec.ts @@ -1,3 +1,5 @@ +import { EndpointBearer, SerdeFunctions } from "@smithy/types"; + import { deserializerMiddleware } from "./deserializerMiddleware"; describe("deserializerMiddleware", () => { @@ -11,7 +13,7 @@ describe("deserializerMiddleware", () => { hostname: "hostname", path: "path", }), - }; + } as EndpointBearer & SerdeFunctions; const mockArgs = { input: { diff --git a/packages/middleware-serde/src/deserializerMiddleware.ts b/packages/middleware-serde/src/deserializerMiddleware.ts index b61bce26d15..ab4c2cf7b11 100644 --- a/packages/middleware-serde/src/deserializerMiddleware.ts +++ b/packages/middleware-serde/src/deserializerMiddleware.ts @@ -5,11 +5,17 @@ import { DeserializeMiddleware, HandlerExecutionContext, ResponseDeserializer, + SerdeFunctions, } from "@smithy/types"; -export const deserializerMiddleware = ( - options: RuntimeUtils, - deserializer: ResponseDeserializer +/** + * @internal + * + * 3rd type parameter is deprecated and unused. + */ +export const deserializerMiddleware = ( + options: SerdeFunctions, + deserializer: ResponseDeserializer ): DeserializeMiddleware => ( next: DeserializeHandler, context: HandlerExecutionContext diff --git a/packages/middleware-serde/src/serdePlugin.ts b/packages/middleware-serde/src/serdePlugin.ts index 419bb2de67d..c6db5c247f7 100644 --- a/packages/middleware-serde/src/serdePlugin.ts +++ b/packages/middleware-serde/src/serdePlugin.ts @@ -8,6 +8,7 @@ import { Provider, RequestSerializer, ResponseDeserializer, + SerdeFunctions, SerializeHandlerOptions, UrlParser, } from "@smithy/types"; @@ -39,14 +40,19 @@ export type V1OrV2Endpoint = { endpoint?: Provider; }; -export function getSerdePlugin( - config: V1OrV2Endpoint, - serializer: RequestSerializer, - deserializer: ResponseDeserializer +/** + * @internal + * + * Note: 2nd type parameter is deprecated and unused. + */ +export function getSerdePlugin( + config: V1OrV2Endpoint & SerdeFunctions, + serializer: RequestSerializer, + deserializer: ResponseDeserializer ): Pluggable { return { applyToStack: (commandStack: MiddlewareStack) => { - commandStack.add(deserializerMiddleware(config as SerDeContext, deserializer), deserializerMiddlewareOption); + commandStack.add(deserializerMiddleware(config, deserializer), deserializerMiddlewareOption); commandStack.add(serializerMiddleware(config, serializer), serializerMiddlewareOption); }, }; diff --git a/packages/middleware-serde/src/serializerMiddleware.spec.ts b/packages/middleware-serde/src/serializerMiddleware.spec.ts index c17f26d1314..3b5c0ec3f52 100644 --- a/packages/middleware-serde/src/serializerMiddleware.spec.ts +++ b/packages/middleware-serde/src/serializerMiddleware.spec.ts @@ -1,4 +1,4 @@ -import { EndpointBearer } from "@smithy/types"; +import { EndpointBearer, SerdeFunctions } from "@smithy/types"; import { serializerMiddleware } from "./serializerMiddleware"; @@ -13,7 +13,7 @@ describe("serializerMiddleware", () => { hostname: "hostname", path: "path", }), - }; + } as EndpointBearer & SerdeFunctions; const mockRequest = { method: "GET", diff --git a/packages/middleware-serde/src/serializerMiddleware.ts b/packages/middleware-serde/src/serializerMiddleware.ts index 95253dc994f..777bc4f1aa8 100644 --- a/packages/middleware-serde/src/serializerMiddleware.ts +++ b/packages/middleware-serde/src/serializerMiddleware.ts @@ -2,6 +2,7 @@ import { EndpointBearer, HandlerExecutionContext, RequestSerializer, + SerdeFunctions, SerializeHandler, SerializeHandlerArguments, SerializeHandlerOutput, @@ -10,9 +11,14 @@ import { import type { V1OrV2Endpoint } from "./serdePlugin"; -export const serializerMiddleware = ( - options: V1OrV2Endpoint, - serializer: RequestSerializer +/** + * @internal + * + * Note: 3rd type parameter is deprecated and unused. + */ +export const serializerMiddleware = ( + options: V1OrV2Endpoint & SerdeFunctions, + serializer: RequestSerializer ): SerializeMiddleware => ( next: SerializeHandler, context: HandlerExecutionContext @@ -28,7 +34,7 @@ export const serializerMiddleware = ; + disableHostPrefix: boolean; +} + +/** + * @public + * + * Serde functions from the client config. + */ +export interface SerdeFunctions { base64Encoder: Encoder; base64Decoder: Decoder; utf8Encoder: Encoder; utf8Decoder: Decoder; streamCollector: StreamCollector; - requestHandler: RequestHandler; - disableHostPrefix: boolean; } /** diff --git a/packages/types/src/util.ts b/packages/types/src/util.ts index 6b4433ef6be..bebb7775669 100644 --- a/packages/types/src/util.ts +++ b/packages/types/src/util.ts @@ -12,15 +12,23 @@ export type Exact = [Type1] extends [Type2] ? ([Type2] extends [Ty /** * @public * - * A function that, given a TypedArray of bytes, can produce a string - * representation thereof. + * A function that, given a Uint8Array of bytes, can produce a string + * representation thereof. The function may optionally attempt to + * convert other input types to Uint8Array before encoding. * * @example An encoder function that converts bytes to hexadecimal - * representation would return `'deadbeef'` when given - * `new Uint8Array([0xde, 0xad, 0xbe, 0xef])`. + * representation would return `'hello'` when given + * `new Uint8Array([104, 101, 108, 108, 111])`. */ export interface Encoder { - (input: Uint8Array): string; + /** + * Caution: the `any` type on the input is for backwards compatibility. + * Runtime support is limited to Uint8Array and string by default. + * + * You may choose to support more encoder input types if overriding the default + * implementations. + */ + (input: Uint8Array | string | any): string; } /** @@ -30,8 +38,8 @@ export interface Encoder { * string. * * @example A decoder function that converts bytes to hexadecimal - * representation would return `new Uint8Array([0xde, 0xad, 0xbe, 0xef])` when - * given the string `'deadbeef'`. + * representation would return `new Uint8Array([104, 101, 108, 108, 111])` when + * given the string `'hello'`. */ export interface Decoder { (input: string): Uint8Array; diff --git a/packages/util-base64/package.json b/packages/util-base64/package.json index c85a95f8f73..7bcdd48cb10 100644 --- a/packages/util-base64/package.json +++ b/packages/util-base64/package.json @@ -23,6 +23,7 @@ "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "workspace:^", + "@smithy/util-utf8": "workspace:^", "tslib": "^2.5.0" }, "devDependencies": { diff --git a/packages/util-base64/src/toBase64.browser.spec.ts b/packages/util-base64/src/toBase64.browser.spec.ts index 914e47c4615..81261f095ef 100644 --- a/packages/util-base64/src/toBase64.browser.spec.ts +++ b/packages/util-base64/src/toBase64.browser.spec.ts @@ -1,6 +1,8 @@ /** * @jest-environment jsdom */ +import type { Encoder } from "@smithy/types"; + import testCases from "./__mocks__/testCases.json"; import { toBase64 } from "./toBase64.browser"; @@ -8,4 +10,13 @@ describe(toBase64.name, () => { it.each(testCases as Array<[string, string, number[]]>)("%s", (desc, encoded, decoded) => { expect(toBase64(new Uint8Array(decoded))).toEqual(encoded); }); + + it("also converts strings", () => { + expect(toBase64("hello")).toEqual("aGVsbG8="); + }); + + it("throws on non-string non-Uint8Array", () => { + expect(() => (toBase64 as Encoder)(new Date())).toThrow(); + expect(() => (toBase64 as Encoder)({})).toThrow(); + }); }); diff --git a/packages/util-base64/src/toBase64.browser.ts b/packages/util-base64/src/toBase64.browser.ts index 41a2695b825..bdfad0f9b15 100644 --- a/packages/util-base64/src/toBase64.browser.ts +++ b/packages/util-base64/src/toBase64.browser.ts @@ -1,12 +1,25 @@ +import { fromUtf8 } from "@smithy/util-utf8"; + import { alphabetByValue, bitsPerByte, bitsPerLetter, maxLetterValue } from "./constants.browser"; + /** - * Converts a Uint8Array of binary data to a base-64 encoded string. + * Converts a Uint8Array of binary data or a utf-8 string to a base-64 encoded string. * - * @param input The binary data to encode + * @param _input - the binary data or string to encode. + * @returns base64 string. * * @see https://tools.ietf.org/html/rfc4648#section-4 */ -export function toBase64(input: Uint8Array): string { +export function toBase64(_input: Uint8Array | string): string { + let input: Uint8Array; + if (typeof _input === "string") { + input = fromUtf8(_input); + } else { + input = _input as Uint8Array; + } + if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") { + throw new Error("@smithy/util-base64: toBase64 encoder function only accepts string | Uint8Array."); + } let str = ""; for (let i = 0; i < input.length; i += 3) { let bits = 0; diff --git a/packages/util-base64/src/toBase64.spec.ts b/packages/util-base64/src/toBase64.spec.ts index 77b144f1b18..ae60b4ffdb5 100644 --- a/packages/util-base64/src/toBase64.spec.ts +++ b/packages/util-base64/src/toBase64.spec.ts @@ -1,3 +1,5 @@ +import type { Encoder } from "@smithy/types"; + import testCases from "./__mocks__/testCases.json"; import { toBase64 } from "./toBase64"; @@ -9,4 +11,13 @@ describe(toBase64.name, () => { it("should throw when given a number", () => { expect(() => toBase64(0xdeadbeefface as any)).toThrow(); }); + + it("also converts strings", () => { + expect(toBase64("hello")).toEqual("aGVsbG8="); + }); + + it("throws on non-string non-Uint8Array", () => { + expect(() => (toBase64 as Encoder)(new Date())).toThrow(); + expect(() => (toBase64 as Encoder)({})).toThrow(); + }); }); diff --git a/packages/util-base64/src/toBase64.ts b/packages/util-base64/src/toBase64.ts index d6c32e0ed74..69c0b70a2d2 100644 --- a/packages/util-base64/src/toBase64.ts +++ b/packages/util-base64/src/toBase64.ts @@ -1,10 +1,22 @@ import { fromArrayBuffer } from "@smithy/util-buffer-from"; +import { fromUtf8 } from "@smithy/util-utf8"; /** - * Converts a Uint8Array of binary data to a base-64 encoded string using + * Converts a Uint8Array of binary data or a utf-8 string to a base-64 encoded string using * Node.JS's `buffer` module. * - * @param input The binary data to encode + * @param _input - the binary data or string to encode. + * @returns base64 string. */ -export const toBase64 = (input: Uint8Array): string => - fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("base64"); +export const toBase64 = (_input: Uint8Array | string): string => { + let input: Uint8Array; + if (typeof _input === "string") { + input = fromUtf8(_input); + } else { + input = _input as Uint8Array; + } + if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") { + throw new Error("@smithy/util-base64: toBase64 encoder function only accepts string | Uint8Array."); + } + return fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("base64"); +}; diff --git a/packages/util-utf8/src/toUtf8.browser.spec.ts b/packages/util-utf8/src/toUtf8.browser.spec.ts index f51d5bcd016..c5d82b0f5b4 100644 --- a/packages/util-utf8/src/toUtf8.browser.spec.ts +++ b/packages/util-utf8/src/toUtf8.browser.spec.ts @@ -1,6 +1,8 @@ /** * @jest-environment jsdom */ +import type { Encoder } from "@smithy/types"; + import { toUtf8 } from "./toUtf8.browser"; declare const global: any; @@ -13,4 +15,13 @@ describe("toUtf8", () => { expect(toUtf8(new Uint8Array(0))).toBe(expected); }); + + it("passes through strings", () => { + expect(toUtf8("hello")).toEqual("hello"); + }); + + it("throws on non-string non-Uint8Array", () => { + expect(() => (toUtf8 as Encoder)(new Date())).toThrow(); + expect(() => (toUtf8 as Encoder)({})).toThrow(); + }); }); diff --git a/packages/util-utf8/src/toUtf8.browser.ts b/packages/util-utf8/src/toUtf8.browser.ts index 872cd66bf87..e1288102425 100644 --- a/packages/util-utf8/src/toUtf8.browser.ts +++ b/packages/util-utf8/src/toUtf8.browser.ts @@ -1 +1,15 @@ -export const toUtf8 = (input: Uint8Array): string => new TextDecoder("utf-8").decode(input); +/** + * + * This does not convert non-utf8 strings to utf8, it only passes through strings if + * a string is received instead of a Uint8Array. + * + */ +export const toUtf8 = (input: Uint8Array | string): string => { + if (typeof input === "string") { + return input; + } + if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") { + throw new Error("@smithy/util-utf8: toUtf8 encoder function only accepts string | Uint8Array."); + } + return new TextDecoder("utf-8").decode(input); +}; diff --git a/packages/util-utf8/src/toUtf8.spec.ts b/packages/util-utf8/src/toUtf8.spec.ts index 153ea1a1d0a..32a3b1610c0 100644 --- a/packages/util-utf8/src/toUtf8.spec.ts +++ b/packages/util-utf8/src/toUtf8.spec.ts @@ -1,3 +1,5 @@ +import type { Encoder } from "@smithy/types"; + import { toUtf8 } from "./toUtf8"; const utf8StringsToByteArrays: Record = { @@ -144,4 +146,13 @@ describe("toUtf8", () => { it("should throw when given a number", () => { expect(() => toUtf8(255 as any)).toThrow(); }); + + it("passes through strings", () => { + expect(toUtf8("hello")).toEqual("hello"); + }); + + it("throws on non-string non-Uint8Array", () => { + expect(() => (toUtf8 as Encoder)(new Date())).toThrow(); + expect(() => (toUtf8 as Encoder)({})).toThrow(); + }); }); diff --git a/packages/util-utf8/src/toUtf8.ts b/packages/util-utf8/src/toUtf8.ts index a192c8409fd..27dd3acb167 100644 --- a/packages/util-utf8/src/toUtf8.ts +++ b/packages/util-utf8/src/toUtf8.ts @@ -1,4 +1,17 @@ import { fromArrayBuffer } from "@smithy/util-buffer-from"; -export const toUtf8 = (input: Uint8Array): string => - fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("utf8"); +/** + * + * This does not convert non-utf8 strings to utf8, it only passes through strings if + * a string is received instead of a Uint8Array. + * + */ +export const toUtf8 = (input: Uint8Array | string): string => { + if (typeof input === "string") { + return input; + } + if (typeof input !== "object" || typeof input.byteOffset !== "number" || typeof input.byteLength !== "number") { + throw new Error("@smithy/util-utf8: toUtf8 encoder function only accepts string | Uint8Array."); + } + return fromArrayBuffer(input.buffer, input.byteOffset, input.byteLength).toString("utf8"); +}; diff --git a/yarn.lock b/yarn.lock index 30ab53f4643..9ed7ab45403 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2554,6 +2554,7 @@ __metadata: resolution: "@smithy/util-base64@workspace:packages/util-base64" dependencies: "@smithy/util-buffer-from": "workspace:^" + "@smithy/util-utf8": "workspace:^" "@tsconfig/recommended": 1.0.1 "@types/node": ^14.14.31 concurrently: 7.0.0