Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(codegen): add serde helper function #4616

Merged
merged 2 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => _;