Skip to content

Commit

Permalink
feat(codegen): add serde helper function (#4616)
Browse files Browse the repository at this point in the history
* feat(codegen): add serde helper function

* feat(codegen): allow non-func for filter instruction
  • Loading branch information
kuhe authored Apr 6, 2023
1 parent e385e85 commit bcb14ba
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 27 deletions.
77 changes: 76 additions & 1 deletion packages/smithy-client/src/object-mapping.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { map, ObjectMappingInstructions } from "./object-mapping";
import { map, ObjectMappingInstructions, SourceMappingInstructions, take } from "./object-mapping";

describe("object mapping", () => {
const example: ObjectMappingInstructions = {
Expand Down Expand Up @@ -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);
});
});
});
122 changes: 96 additions & 26 deletions packages/smithy-client/src/object-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@
*/
export type ObjectMappingInstructions = Record<string, ObjectMappingInstruction>;

/**
* @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<string, ValueMapper | SourceMappingInstruction>;

/**
* @internal
*
Expand Down Expand Up @@ -80,6 +89,10 @@ export type SimpleValueInstruction = [FilterStatus, Value];
* @internal
*/
export type ConditionalValueInstruction = [ValueFilteringFunction, Value];
/**
* @internal
*/
export type SourceMappingInstruction = [ValueFilteringFunction?, ValueMapper?, string?];

/**
* @internal
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -139,11 +160,11 @@ export function map(
/**
* @internal
*/
export function map(instructions: Record<string, ObjectMappingInstruction>): any;
export function map(instructions: ObjectMappingInstructions): any;
/**
* @internal
*/
export function map(target: any, instructions: Record<string, ObjectMappingInstruction>): typeof target;
export function map(target: any, instructions: ObjectMappingInstructions): typeof target;
/**
* @internal
*/
Expand Down Expand Up @@ -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;
}
Expand All @@ -213,6 +211,20 @@ export const convertMap = (target: any): Record<string, any> => {
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.
*
Expand Down Expand Up @@ -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<string, SourceMappingInstruction>,
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) => _;

0 comments on commit bcb14ba

Please sign in to comment.