-
Notifications
You must be signed in to change notification settings - Fork 4
/
polymorphic.ts
183 lines (157 loc) · 6.17 KB
/
polymorphic.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// Copyright 2018-2021 Gamebridge.ai authors. All rights reserved. MIT license.
import { JSONValue, Serializable } from "./serializable.ts";
import {
ERROR_FAILED_TO_RESOLVE_POLYMORPHIC_CLASS,
ERROR_MISSING_STATIC_OR_VALUE_ON_POLYMORPHIC_SWITCH,
} from "./error_messages.ts";
/** Polymorphic class deserializer
*
* There are currently 2 ways of doing polymorphic deserialization:
* 1. Manually using @PolymorphicResolver on a static method on the parent class
*
* This works by keeping a map of target 'parent' classes to resolver functions.
* These are set when a static method is annotated with @PolymorphicResolver.
* You can then call `serializePolymorphicClass` with the parent class and an
* input the input is passed to whatever the corresponding resolver function,
* which will make a determination and returns an instance of a 'child' class
*
* 2. Implicitly using @PolymorphicSwitch on a static or instance property on a child class.
*
* This works by getting the decorated class' parent prototype and creating a map
* for a specific class, property key, and value combination to the provided
* initializer function
*/
/** @PolymorphicResolver method decorator */
export function PolymorphicResolver(
target: unknown,
propertyKey: string | symbol,
): void {
registerPolymorphicResolver(
target,
(target as Record<typeof propertyKey, () => Serializable>)[
propertyKey as string
],
);
}
export type ResolverFunction = (
input: string | JSONValue | Object,
) => Serializable;
/** Map of class constructors to functions that take in a JSON input and output a class instance that inherits Serializable */
const POLYMORPHIC_RESOLVER_MAP = new Map<unknown, ResolverFunction>();
/** Adds a class and a resolver function to the resolver map */
function registerPolymorphicResolver(
classPrototype: unknown,
resolver: ResolverFunction,
): void {
POLYMORPHIC_RESOLVER_MAP.set(classPrototype, resolver);
}
/** @PolymorphicSwitch property decorator
* Note: This will only create de-serializer logic for the target class' direct parent class
*/
export function PolymorphicSwitch(
initializerFunction: InitializerFunction,
value?: unknown,
): PropertyDecorator {
// Because `undefined` can be used as a value here, we need to check if the argument was even set
const hasValue = arguments.hasOwnProperty("1");
return function _PolymorphicSwitch(
target: Function | Object, // The constructor of the class for static properties, and the class it's self for instance properties
propertyKey: string | symbol,
) {
// Assert property should be static
if (
!Object.prototype.hasOwnProperty.call(target, propertyKey) &&
!hasValue
) {
throw new Error(ERROR_MISSING_STATIC_OR_VALUE_ON_POLYMORPHIC_SWITCH);
}
let targetConstructor = target;
if (typeof target !== "function") {
targetConstructor = target.constructor;
}
const parentPrototype = Object.getPrototypeOf(targetConstructor);
// BUG: propertyKey can also be symbol, but typescript/deno throws an error due to https://github.com/microsoft/TypeScript/issues/1863
const propertyValue = (target as Record<typeof propertyKey, unknown>)[
(propertyKey as string)
] || value;
registerPolymorphicSwitch(
parentPrototype,
propertyKey,
propertyValue,
initializerFunction,
);
};
}
export type InitializerFunction = () => Serializable;
/** Parent constructor -> property key -> value -> initializer */
const POLYMORPHIC_SWITCH_MAP = new Map<
unknown,
Map<string | symbol, Map<unknown, InitializerFunction>>
>();
/** Add an initializer function or a specific combination of parent prototype, property key, and value */
function registerPolymorphicSwitch(
parentPrototype: unknown,
propertyKey: string | symbol,
propertyValue: unknown,
initializerFunction: InitializerFunction,
): void {
// Get map for parent prototype, or initialize if it doesn't exist
let classMap = POLYMORPHIC_SWITCH_MAP.get(parentPrototype);
if (!classMap) {
POLYMORPHIC_SWITCH_MAP.set(parentPrototype, new Map());
classMap = POLYMORPHIC_SWITCH_MAP.get(parentPrototype);
}
// Get map for property key, or initialize if it doesn't exist
let propertyKeyMap = classMap?.get(propertyKey);
if (!propertyKeyMap) {
classMap?.set(propertyKey, new Map());
propertyKeyMap = classMap?.get(propertyKey);
}
// Add value to initializer mapping
propertyKeyMap?.set(propertyValue, initializerFunction);
}
/** Uses either the polymorphic resolver or the polymorphic switch resolver to determine the
* appropriate class, then deserialize the input using Serializable#fromJSON, returning the result
*/
export function polymorphicClassFromJSON<T extends Serializable>(
classPrototype: Object & { prototype: T },
input: string | JSONValue | Object,
): T {
return resolvePolymorphicClass(classPrototype, input).fromJSON(input);
}
/** Calls the polymorphic resolver or polymorphic switch resolver for the provided class prototype
* and input, and returns the initialized child class. Throws an exception
*/
function resolvePolymorphicClass<T extends Serializable>(
classPrototype: Object & { prototype: T },
input: string | JSONValue | Object,
): T {
const classResolver = POLYMORPHIC_RESOLVER_MAP.get(classPrototype);
if (classResolver) {
return classResolver(input) as T;
}
const resolvedClass = resolveSwitchMap(classPrototype, input);
if (resolvedClass) {
return resolvedClass as T;
}
throw new Error(ERROR_FAILED_TO_RESOLVE_POLYMORPHIC_CLASS);
}
/** Return a resolved class type by checking types on the input. Currently the input is simply `JSON.parse` */
function resolveSwitchMap(
classPrototype: unknown,
input: string | JSONValue | Object,
): Serializable | null {
const classMap = POLYMORPHIC_SWITCH_MAP.get(classPrototype);
if (!classMap) {
throw new Error(ERROR_FAILED_TO_RESOLVE_POLYMORPHIC_CLASS);
}
const inputObject = typeof input === "string" ? JSON.parse(input) : input;
for (const [propertyKey, valueMap] of classMap.entries()) {
const value = inputObject[propertyKey];
const initializer = valueMap.get(value);
if (initializer) {
return initializer();
}
}
return null;
}