diff --git a/packages/smithy-client/src/object-mapping.spec.ts b/packages/smithy-client/src/object-mapping.spec.ts index ad0d190466d9..ba169c769ee1 100644 --- a/packages/smithy-client/src/object-mapping.spec.ts +++ b/packages/smithy-client/src/object-mapping.spec.ts @@ -1,4 +1,4 @@ -import { map, ObjectMappingInstructions } from "./object-mapping"; +import { map, ObjectMappingInstructions, SourceMappingInstructions, take } from "./object-mapping"; describe("object mapping", () => { const example: ObjectMappingInstructions = { @@ -128,4 +128,79 @@ describe("object mapping", () => { ).toEqual({}); }); }); + + describe("take function", () => { + it("will not apply instructions to missing fields", () => { + const input = { + filteredDefault: null, + filteredSupplier: undefined, + filteredMapper: void 0, + filteredFilter: 43, + filteredMapperOnly: null, + } as const; + + const output = {} as const; + + const instructions: SourceMappingInstructions = { + default: [], + filteredDefault: [], + supplier: [, () => "x"], + filteredSupplier: [, () => "x"], + mapper: [, (_) => _ + "x"], + filteredMapper: [, (_) => _ + "x"], + filter: [(_) => _ === 42], + filteredFilter: [(_) => _ === 42], + sourceKey: [, , "SOURCE_KEY"], + sourceKey2: [, (_) => "mapped" + _, "SOURCE_KEY2"], + mapperOnly: (_) => _ + "Only", + filteredMapperOnly: (_) => _ + "Only", + }; + + expect(take(input, instructions)).toEqual(output); + }); + + it("should take keys with optional filters and optional mappers", () => { + const input = { + default: 0, + filteredDefault: null, + supplier: false, + filteredSupplier: undefined, + mapper: "y", + filteredMapper: void 0, + filter: 42, + filteredFilter: 43, + SOURCE_KEY: "SOURCE_VALUE", + SOURCE_KEY2: "SOURCE_VALUE2", + mapperOnly: "mapper", + filteredMapperOnly: null, + } as const; + + const output = { + default: 0, + supplier: "x", + mapper: "yx", + filter: 42, + sourceKey: "SOURCE_VALUE", + sourceKey2: "mappedSOURCE_VALUE2", + mapperOnly: "mapperOnly", + } as const; + + const instructions: SourceMappingInstructions = { + default: [], + filteredDefault: [], + supplier: [, () => "x"], + filteredSupplier: [, () => "x"], + mapper: [, (_) => _ + "x"], + filteredMapper: [, (_) => _ + "x"], + filter: [(_) => _ === 42], + filteredFilter: [(_) => _ === 42], + sourceKey: [, , "SOURCE_KEY"], + sourceKey2: [, (_) => "mapped" + _, "SOURCE_KEY2"], + mapperOnly: (_) => _ + "Only", + filteredMapperOnly: (_) => _ + "Only", + }; + + expect(take(input, instructions)).toEqual(output); + }); + }); }); diff --git a/packages/smithy-client/src/object-mapping.ts b/packages/smithy-client/src/object-mapping.ts index fe44920a14a8..6cf80a5f1a1e 100644 --- a/packages/smithy-client/src/object-mapping.ts +++ b/packages/smithy-client/src/object-mapping.ts @@ -46,6 +46,15 @@ */ export type ObjectMappingInstructions = Record; +/** + * @internal + * + * A variant of the object mapping instruction for the `take` function. + * In this case, the source value is provided to the value function, turning it + * from a supplier into a mapper. + */ +export type SourceMappingInstructions = Record; + /** * @internal * @@ -80,6 +89,10 @@ export type SimpleValueInstruction = [FilterStatus, Value]; * @internal */ export type ConditionalValueInstruction = [ValueFilteringFunction, Value]; +/** + * @internal + */ +export type SourceMappingInstruction = [ValueFilteringFunction?, ValueMapper?, string?]; /** * @internal @@ -112,6 +125,14 @@ export type ValueFilteringFunction = (value: any) => boolean; */ export type ValueSupplier = () => any; +/** + * @internal + * + * A function that maps the source value to the target value. + * Defaults to pass-through with nullish check. + */ +export type ValueMapper = (value: any) => any; + /** * @internal * @@ -139,11 +160,11 @@ export function map( /** * @internal */ -export function map(instructions: Record): any; +export function map(instructions: ObjectMappingInstructions): any; /** * @internal */ -export function map(target: any, instructions: Record): typeof target; +export function map(target: any, instructions: ObjectMappingInstructions): typeof target; /** * @internal */ @@ -171,30 +192,7 @@ export function map(arg0: any, arg1?: any, arg2?: any): any { target[key] = instructions[key]; // unchecked value. continue; } - - // eslint-disable-next-line prefer-const - let [filter, value]: [((_?: any) => boolean) | unknown, any] = instructions[key]; - - if (typeof value === "function") { - let _value: any; - const defaultFilterPassed = filter === undefined && (_value = value()) != null; - const customFilterPassed = - (typeof filter === "function" && !!filter(void 0)) || (typeof filter !== "function" && !!filter); - - if (defaultFilterPassed) { - target[key] = _value; - } else if (customFilterPassed) { - target[key] = value(); - } - } else { - const defaultFilterPassed = filter === undefined && value != null; - const customFilterPassed = - (typeof filter === "function" && !!filter(value)) || (typeof filter !== "function" && !!filter); - - if (defaultFilterPassed || customFilterPassed) { - target[key] = value; - } - } + applyInstruction(target, null, instructions, key); } return target; } @@ -213,6 +211,20 @@ export const convertMap = (target: any): Record => { return output; }; +/** + * @param source - original object with data. + * @param instructions - how to map the data. + * @returns new object mapped from the source object. + * @internal + */ +export const take = (source: any, instructions: SourceMappingInstructions): any => { + const out = {}; + for (const key in instructions) { + applyInstruction(out, source, instructions, key); + } + return out; +}; + /** * Private, for codegen use only. * @@ -251,3 +263,61 @@ const mapWithFilter = ( ) ); }; + +/** + * @internal + * + * Applies a single instruction at the given key from source to target. + */ +const applyInstruction = ( + target: any, + source: null | any, + instructions: ObjectMappingInstructions | Record, + targetKey: string +): void => { + if (source !== null) { + let instruction = instructions[targetKey]; + if (typeof instruction === "function") { + instruction = [, instruction]; + } + const [filter = nonNullish, valueFn = pass, sourceKey = targetKey] = instruction; + if ((typeof filter === "function" && filter(source[sourceKey])) || (typeof filter !== "function" && !!filter)) { + target[targetKey] = valueFn(source[sourceKey]); + } + return; + } + + // eslint-disable-next-line prefer-const + let [filter, value]: [((_?: any) => boolean) | unknown, any] = instructions[targetKey]; + + if (typeof value === "function") { + let _value: any; + const defaultFilterPassed = filter === undefined && (_value = value()) != null; + const customFilterPassed = + (typeof filter === "function" && !!filter(void 0)) || (typeof filter !== "function" && !!filter); + + if (defaultFilterPassed) { + target[targetKey] = _value; + } else if (customFilterPassed) { + target[targetKey] = value(); + } + } else { + const defaultFilterPassed = filter === undefined && value != null; + const customFilterPassed = + (typeof filter === "function" && !!filter(value)) || (typeof filter !== "function" && !!filter); + + if (defaultFilterPassed || customFilterPassed) { + target[targetKey] = value; + } + } +}; + +/** + * internal + */ +const nonNullish = (_: any) => _ != null; + +/** + * internal + */ +const pass = (_: any) => _;